Docyrus Webform Design
Design a public webform with docyrus studio create-webform, then validate it and test that a submission lands as a record. A webform is a tenant_webform row, paired 1:1 with a webhook, that renders a public (no-login) form; each submission creates a record in the bound data source (or, if unbound, in a per-tenant webform_record table) and can fire an automation.
How a webform works (read first)
- A webform is bound to a data source at create time. Each form field's slug must match a field slug on that data source — that's how a submission maps to record columns. Bind the data source first (see docyrus-data-source-design).
- Studio/builder-compatible forms persist two JSON documents:
schemafor ordered fields and per-field layout, andoptionsfor layout variant, theme, grid/wizard settings, and respondent-facing copy. The authoritative contract is references/webform-design-schema.md. - The public form is addressed by the paired webhook's short id + token, not the webform UUID. After create, read back
form_url(render),form_submit_url(POST target), andembed_code(a<script>snippet). - A submission
POSTs{ "data": { …field values… } }to the submit URL → it's queued → an edge function asynchronously creates the record in the bound data source and fires any active automationwebformtrigger. So records appear a moment after submission, not synchronously.
Workflow
-
Confirm app + auth, and the target data source. The form writes into one data source whose field slugs the form keys must match.
docyrus auth who --json docyrus apps list --json docyrus studio list-fields --appSlug crm --dataSourceSlug leads --json # the slugs your form fields must use -
Design the form schema. Decide which data-source fields the form collects, then build the separate
schemaandoptionsdocuments. Eachschema.children[].field.slug= the data-source field slug. Read references/webform-design-schema.md for the complete builder contract and start from references/examples/webform-design.full.example.json when a full example is useful. Use references/schemas/webform-builder.schema.json and references/schemas/webform-options.schema.json for machine validation. Use references/webform-model-and-submission.md for the backend lifecycle and record-mapping details. -
Create the webform (
create-webform) bound to the data source,status: 1(active). See Create. -
Get the public URLs from the response/
get-webform(form_url,form_submit_url,embed_code). -
Validate the shape + binding. See Validate.
-
Test a submission and confirm a record is created in the data source. See Test.
A worked example (a "Leads" intake form bound to a CRM data source, submitted, record confirmed) is in references/webform-model-and-submission.md.
Builder schema specification (read when authoring JSON)
Use the copied Webforms builder specification as the source of truth whenever the task requires a studio-compatible form design rather than only a minimal backend payload:
- Read references/webform-design-schema.md for
schema,options, all 30 palette field types, validation tokens, value shapes, derivedjson_schema, submission behavior, and renderer compatibility notes. - Validate the two persisted documents independently with references/schemas/webform-builder.schema.json and references/schemas/webform-options.schema.json.
- Use references/schemas/webform-design-output.schema.json only for a documentation/test wrapper containing both documents; the API stores them as separate properties.
- Validate the public POST wrapper with references/schemas/webform-submission-envelope.schema.json, then validate its
dataproperty against the webform-specific derivedjson_schema. - Use references/examples/webform-design.full.example.json as the exhaustive palette example. Reduce it to the fields the user needs; do not copy unrelated fields into a production form.
The native builder shape is { "version": 1, "children": [{ "type", "field", "enumOptions"?, "layout" }] }. Do not substitute the older FieldText + options.key/options.label shape when the user asks for output compatible with the current Webforms builder.
Command cheat-sheet
Selectors: --appId | --appSlug (to resolve a slug), --dataSourceId | --dataSourceSlug (the binding), --webformId (the form). Write commands take camelCase flags or --data/--from-file; flags merge over JSON. Append --json.
⚠️ Webforms have no slug —
get/update/deleteaddress the form only by--webformId.list-webformscan filter by data source.
Create a webform
docyrus studio create-webform --appSlug crm --dataSourceSlug leads \
--name "Website lead form" --status 1 \
--schema '{"version":1,"children":[
{"type":"field-text","field":{"id":"lead-name","name":"Name","slug":"name","type":"field-text","validations":["required"]},"layout":{"colSpan":1,"rowSpan":1}},
{"type":"field-email","field":{"id":"lead-email","name":"Email","slug":"email","type":"field-email","validations":["required"]},"layout":{"colSpan":1,"rowSpan":1}}
]}' \
--css "body{font-family:sans-serif}" --json
# → capture data.id (webform UUID) and data.form_url / data.form_submit_url / data.embed_code
- Required:
--name,--schema(a non-empty JSON object), and--status(1 = active, 2 = inactive). Omitting any → validation error (the CLI marks them optional, but the backend rejects). - Binding is optional but create-only:
--dataSourceSlug/--dataSourceIdsets the data source. ⚠️update-webformcannot rebind — to change the data source, recreate the form. An unbound form still captures submissions (intowebform_record), just not into a data source. --webformOptions(JSON) and--cssare optional builder config;--sandbox truemarks it a test form (separate sandbox URL).- Each
schema.children[].field.slugmust equal a bound data-source field slug — that's what makes a submission populate the record. Confirm withlist-fieldsfirst. Also require non-empty unique field slugs and stable field/option/grid-row IDs as specified in references/webform-design-schema.md.
Manage / inspect
docyrus studio list-webforms --appSlug crm --dataSourceSlug leads --json # filter by data source; omit to list all
docyrus studio get-webform --webformId <id> --json # full schema + form_url/form_submit_url/embed_code
docyrus studio update-webform --webformId <id> --status 2 --json # PATCH; partial; NO data-source rebind
docyrus studio delete-webform --webformId <id> --json # 204; also deletes the paired webhook
get/create responses expose the public handles:form_id/form_token(the webhook short id + token),form_url(render),form_submit_url(POST target),form_sandbox_url, andembed_code(a ready<script>snippet).updateis PATCH (partial).deletereturns 204 and cascades to the paired webhook.
Critical rules
name+schema(JSON object) +status(1|2) are required on create;statusmust be1or2(any other value rejected).schema.children[].field.slug= data-source field slug. A submission'sdatakeys are mapped to records by slug — mismatched slugs won't populate the record. Validate slugs withlist-fieldsbefore authoring the schema and follow the builder specification's uniqueness/stability rules.- Binding is create-only.
--dataSourceSlug/--dataSourceIdonly applies oncreate-webform;update-webformhas no rebind. Recreate to move a form to a different data source. - Two CLI casing traps: in a raw
--datapayload, the data-source key isdataSourceId(camelCase, nottenant_data_source_id), and the options key isoptionseven though the flag is--webformOptions. Prefer the flags, which map correctly. - Public address = webhook id + token, not the webform UUID. Use
form_url/form_submit_url/embed_codefrom the response; don't construct URLs from the webform id. - Submissions create records asynchronously (queued → edge function). Expect a short delay; a record isn't created in the same request. An empty/empty-object
dataPOST is treated as a verification ping and stores nothing. - Unbound webforms are valid — submissions go to the per-tenant
webform_recordtable instead of a data source (queryable via thewebforms/{id}/itemsAPI). - Submission fires automations: an active automation
webformtrigger bound to this form (--webformId) runs on each submission (see docyrus-automation-design). - Validate then test. Confirm the schema/binding, then submit once and confirm the record. Delete throwaway forms (and the records they created).
Validate
docyrus studio get-webform --webformId <id> --json—name,status,schema,tenant_data_source_id(admin field) as intended;form_url/form_submit_url/embed_codepresent.- Validate
schemaandoptionsagainst the copied machine schemas, then cross-check everyschema.children[].field.slugagainstdocyrus studio list-fields --appSlug crm --dataSourceSlug leads --json— each maps to a real field slug (unmapped slugs silently won't populate the record). docyrus studio list-webforms --appSlug crm --dataSourceSlug leads --json— the form appears under its bound data source.
Test
Prove a submission becomes a record:
- Submit to the public endpoint (no auth needed — the id+token is the credential). Use
form_submit_urlfromget-webform:curl -X POST "<form_submit_url>" -H 'Content-Type: application/json' \ -d '{"data":{"name":"Jane Tester","email":"jane@example.com"}}' # → { "id": ..., "created_at": ... } (queued; record is created asynchronously) - Confirm the record lands in the bound data source (allow a moment for the queue/edge function):
For an unbound form, read submissions viadocyrus ds list crm leads --columns "name, email" \ --filters '{"rules":[{"field":"email","operator":"=","value":"jane@example.com"}]}' --jsondocyrus curl "/v1/webforms/<webformId>/items"instead. - Clean up: delete the test record(s), then
docyrus studio delete-webform --webformId <id> --json.
Submission → record runs through an async queue + edge function; in a local dev environment that pipeline may not execute, so the submit may return
{id}without a record appearing. Treat a cleancreate/get(with a validform_submit_url) and matching field slugs as the schema-correctness check, and run the full submission round-trip in a real environment.
Full submission/record lifecycle details and the webform_record items API are in references/webform-model-and-submission.md. The current builder-generated schema/options contract is in references/webform-design-schema.md.
References
- references/webform-design-schema.md — Authoritative Webforms builder specification: persisted
schemaandoptions, all 30 palette types, validation/value contracts, derivedjson_schema, submission envelope, round-trip rules, and current renderer compatibility gaps. - references/schemas/webform-builder.schema.json — Draft 2020-12 schema for the persisted builder
schemadocument. - references/schemas/webform-options.schema.json — Draft 2020-12 schema for normalized persisted
options. - references/schemas/webform-design-output.schema.json — Documentation wrapper that references both persisted-document schemas.
- references/schemas/webform-submission-envelope.schema.json — Static schema for
{ data, turnstileToken? }; validatedataagainst the form-specific derivedjson_schema. - references/examples/webform-design.full.example.json — Complete example containing every native builder palette type.
- references/webform-model-and-submission.md — Backend webform model, CLI/API casing, public submission flow, bound vs unbound record mapping, response handles, automation linkage, and worked lifecycle example. For the current builder-generated JSON shape, defer to
webform-design-schema.md. - docyrus-data-source-design — the data source the form writes into (its field slugs = the form field slugs). docyrus-automation-design — the
webformtrigger that fires on submission. docyrus-cli-app — CLI command index;docyrus studio …-webform --helpfor flags.