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_email and pauses on a PbotApproval.
  • Its id, wfl-….

Step 1: Create the form from a schema

bash
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

text
{ "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.


The public link is https://<gateway>/forms/<formId>. No account, no key. Open it, fill it in, submit.

The reply is 201 with:

json
{
  "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:

bash
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:

bash
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.

Through the gateway, the same call is `POST /sdk/forms/generate` under an SDK session.

Step 4: Embed it in a product

Load the SDK once and add a launcher:

html
<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:

html
<hyphen-form form-id="frm-…" done-text="Received. We will be in touch."></hyphen-form>

Listen for the submission:

javascript
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:

bash
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:created with kind: "form";
  • the webhooks carry form_pause_created, then form_submitted or form_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