Forms that start work
Time: 20 minutes
What you'll build: an intake form that starts a workflow on every submission, shared as a link, then embedded in a product with a button, with the run visible in the task sidebar and in Process Studio when it pauses.
Base URL used below: https://your-hyphen.example.com for engine calls with X-Org-Id, and https://<gateway> for the hosted page and the SDK.
Prerequisites
- A published workflow to start. Any workflow works; this guide assumes one that reads
@input.amount_due,@input.reason, and@input.contact_emailand pauses on aPbotApproval. - Its id,
wfl-ā¦.
Step 1: Create the form from a schema
curl -X POST https://your-hyphen.example.com/external-forms \
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{
"name": "Vendor exception intake",
"description": "Tell us about the charge you are disputing.",
"is_public": true,
"trigger_workflow_id": "wfl-ā¦",
"schema": {
"version": "form_schema_v1",
"title": "Dispute a charge",
"pages": [
{ "id": "charge", "title": "The charge", "fields": [
{ "key": "amount_due", "type": "money", "label": "Disputed amount", "required": true },
{ "key": "reason", "type": "multiline", "label": "Why you are disputing it", "required": true }
] },
{ "id": "contact", "title": "How to reach you", "fields": [
{ "key": "contact_email", "type": "email", "label": "Email", "required": true }
] }
]
},
"settings": { "requireAuth": false, "successMessage": "Thanks. We will be in touch." }
}'
The reply carries the form id. Use trigger_process_id instead of trigger_workflow_id to start a published Process Studio case; never both.
The contract
{ "version": "form_schema_v1", "title"?, "description"?,
"pages": [ { "id", "title"?, "description"?, "fields": [ Field ] } ] }
Field: { "key": snake_case, unique across the form,
"type": text | number | email | date | multiline | content | select | checklist | range | money
| file | signature | table | lookup | computed,
"label", "help"?, "placeholder"?, "required"?, "default"?,
"options"?: [ { "label", "value" } ],
"min"?, "max"?, "step"?, "unit"?, "multiple"?, "masked"? }
The submitted payload is flat: { key: value }. content fields carry guidance text and submit nothing. file, signature, table, lookup, and computed are accepted by the contract; the current renderer shows them as placeholders and never requires them.
The renderer also accepts the legacy builder's component array, a flat field list, and a JSON-Schema object.
Step 2: Share the link
The public link is https://<gateway>/forms/<formId>. No account, no key. Open it, fill it in, submit.
The reply is 201 with:
{
"submission_id": "sub_ā¦",
"triggered_run_id": "run-ā¦",
"triggered_execution_type": "workflow_run"
}
The organization's webhooks receive external_form_submitted with the same fields. Read the run:
curl https://your-hyphen.example.com/runs/run-ā¦/status -H "X-Org-Id: acme-corp"
It is paused at the approval. The approver sees it in their task list; deciding there resumes the run.
Step 3: Create a form from a sentence
Instead of writing the schema, ask for it:
curl -X POST https://your-hyphen.example.com/forms/generate \
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{
"intent": "Collect a vendor exception dispute: the charge, the disputed amount, why, references, and how to reach them. Two short pages.",
"expected_keys": ["amount_due", "reason", "contact_email"],
"create": { "name": "Vendor dispute intake", "is_public": true, "trigger_workflow_id": "wfl-ā¦" }
}'
The reply is { schema, source, attempts, form }. Without create, only the schema comes back for review. The generator validates its output against the contract, retries once with the validator's errors, and falls back to one text field per expected key.
The organization needs the agent_forms feature. Without it the call answers 403 agent_forms_disabled. Ask Hyphen to enable it for your organization.
Step 4: Embed it in a product
Load the SDK once and add a launcher:
<script src="https://<gateway>/sdk/v1.js"></script>
<script>
HyphenSDK.init({ baseUrl: 'https://<gateway>', publishableKey: 'pk_live_ā¦' });
</script>
<hyphen-form-button form-id="frm-ā¦" public>Dispute a charge</hyphen-form-button>
Or render it inline:
<hyphen-form form-id="frm-ā¦" done-text="Received. We will be in touch."></hyphen-form>
Listen for the submission:
document.addEventListener('hyphen-form:submitted', (e) => {
const { triggered_run_id } = e.detail.response;
console.log('started', triggered_run_id);
});
The full attribute and event tables are on the Forms page.
Step 5: Know who answered
Turn on sign-in and brand the page:
curl -X PUT https://your-hyphen.example.com/external-forms/frm-⦠\
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{ "settings": { "requireAuth": true, "branding": { "name": "Northwind Freight", "accent": "#2E6B4E", "footer": "Northwind Freight vendor desk" } } }'
The form stays visible. When the person submits it, the hosted page asks for an email and a one-time code, then continues the submission after sign-in. The triggered run records submitted_by, and so does the webhook.
What a case waiting on a form looks like
When a running case reaches a PbotForm step, the pause is a task:
<hyphen-task-sidebar>lists it beside approvals with Fill it in and renders the form inline;- Process Studio shows Fill it in on the paused case;
- the event stream carries
task:createdwithkind: "form"; - the webhooks carry
form_pause_created, thenform_submittedorform_expired.
Submitting a paused step requires a signed-in session; anonymous sessions can read the form but not settle it.
What You Built
- an intake form from a schema, and one from a sentence
- a public link that starts a governed run on every submission
- the same form inside a product, from a button and inline
- sign-in and branding on the hosted page
- the paused-case path, where a form is a task
ā Next: Embedding Hyphen