Build your first workflow
Draw a process, branch on a condition, stop for a human decision, arm the trigger - and get every run recorded step by step.
Before you begin
By the end of this page you will have a workflow that starts on its own, branches on a condition, pauses for a person, and finishes — with every run recorded.
Pick a process that already exists and is currently done by hand or by a script nobody wants to touch. Invoice approval over a threshold is a good first one: it has a real branch and a real human decision.
How the engine works — read this first
One paragraph that will save you a redesign.
A run follows exactly one path. Steps are connected by links, each link can carry a condition, and the first matching link wins. A link with no condition is the default path. There is no parallel branching, no fan-out, and no wait-all join — if you have designed around running several steps at once, that design will not work here.
Within a single step, actions run in order and chain: each one's output is available to the actions after it, and to later steps.
A malformed condition refuses to match rather than guessing. A mistake stops the run instead of quietly taking the wrong branch.
Step 1 — Draw the process
Lay steps out on the canvas and connect them. Step types cover calling a service, asking a person, and waiting for something. Per step you can set a timeout, a priority, and whether it may be skipped.
Links carry conditions with an explicit order. Conditions compare values, match against a list, check containment, or check existence, and combine with all, any and not.
Loops are first class. You can link back to an earlier step for rework, guarded by a re-entry cap that fails the run loudly rather than spinning forever.
Step 2 — Add a human decision
Make one step wait for a person. The task appears in an inbox, and the answer chooses which link the workflow follows.
Decide who may answer. Approval rules cover one named person, anyone in a role, a named list, a quorum, a weighted vote, any-of, or all-of.
Define your own decision vocabulary and reason codes rather than accepting generic approve/reject — the words your process actually uses are what will appear in the audit later.
Useful behaviours:
- Claims are leased, and an abandoned claim is released automatically, so one person cannot silently sit on a queue.
- The decision options and the form are fixed when the task is created. Editing the workflow later never reshapes a task somebody has already opened.
- Deadline warnings and escalation handle a decision that takes too long.
- Optionally lock the record while it is under review.
When the approval has to stand up to scrutiny
For regulated approval, a decision can require the person to re-authenticate at the moment of signing — with a second factor where it is warranted — and to state a reason from codes you define. Signatures are appended and chained so alteration is detectable, and the decision payload becomes immutable once recorded.
Use this where you would otherwise be asked to prove not just that something was approved, but who approved it and that they proved who they were at that moment.
Step 3 — Validate
Before a version goes live it is checked for:
- steps that cannot be reached
- paths that never terminate
- human decisions that lead nowhere — every possible answer must have a link that handles it
- step types missing fields they need to run
A structurally broken workflow is refused at start time as well as at publish time. This check is doing you a favour; do not work around it.
Step 4 — Publish a version
Publishing freezes the version. In-flight runs follow the version they started on, so changing the workflow never rewrites what is already running. The draft stays editable.
You can start from a template gallery, and save your own workflow back as a template.
Step 5 — Decide what starts it
A binding connects a trigger to the workflow. Triggers:
- a record being created, updated, deleted or promoted to a new version
- a signed HTTPS call from any external system, with per-source secrets you rotate
- a schedule — cron, a fixed interval, or once at a set time
- by hand from the portal, or programmatically with a scoped API key
Add a filter so only the records you care about start a run.
The arming gate. A binding will not fire until its field mappings are complete. If your trigger is not firing, check that it is armed before checking anything else — this is deliberate, because a half-configured trigger that fires is worse than one that refuses to.
Two protections you get for free: repeated triggers for the same subject return the run already in progress rather than starting a second one, and repeat deliveries of the same event are recognised and ignored.
Start a workflow when another workflow completes
A hand-off starts one workflow when a run of another workflow in the same app instance completes — for example Monitor, then Assess, then Track. It is an ordinary binding with a reserved shape, so you create, arm, pause and delete it like any other.
POST /v1/automation/portal/workflow-bindings/
{
"workflow_id": 42,
"subject_type": "source",
"subject_filter": { "source_system": "automation", "workflow_id": 17 },
"trigger": "event",
"trigger_config": { "event_types": ["automation.workflow.completed"] }
}workflow_id is the workflow that starts. subject_filter.workflow_id is the workflow whose completion it waits for. source_system is the fixed value automation, and needs no external system to be registered.
Like every new binding it is created in status draft and starts nothing until you call POST /v1/automation/portal/workflow-bindings/{binding_id}/arm.
A binding that names its own workflow, or that would close a loop, is refused when you create it. Anything that could never fire is refused with a 422 naming each field.
The completion event
The executor emits automation.workflow.completed when a run completes. A run that fails emits automation.workflow.failed instead, which a hand-off does not listen to. The payload carries the run, its workflow and its lineage:
{
"tenant_guid": "…",
"app_instance_guid": "…",
"run_guid": "9f1c…",
"workflow_id": 17,
"workflow_code": "monitor_changes",
"started_at": "2026-11-13T08:00:01Z",
"completed_at": "2026-11-13T08:04:30Z",
"duration_ms": 269000,
"parent_run_guid": null,
"root_run_guid": "9f1c…",
"outcome_code": "…",
"outcome_reason": "…",
"occurred_at": "2026-11-13T08:04:30Z"
}| Field | Meaning |
|---|---|
parent_run_guid | The run that started this one — the run that handed off, or the run whose Run another workflow step started it. null for a run nothing started. |
root_run_guid | The first run of the chain. A run that nothing started is its own root, so this is never null on a completed run. |
The event's JSON schema is permissive (additionalProperties: true) and requires only tenant_guid, run_guid and occurred_at; the lineage fields are present in the payload but are not declared in the schema, so do not rely on a schema validator to require them.
How a hand-off starts the next run
When the completion arrives, the platform starts every armed hand-off binding that names the completed workflow. Three rules decide what happens:
- Same instance only. The completion is evaluated only in the app instance that ran it. Workflow ids are per instance, so the same id in another instance names a different workflow.
- Once per run. The idempotency key is
handoff:<binding guid>:run:<parent run guid>, keyed by the run that handed off and not by the event that carried the news. A completion re-emitted under a new event id still starts the next run once. - The new run records where it came from. The start carries
parent_run_guidin its metadata. The executor accepts it only when that run belongs to the same organisation and app instance, then reads the root from the parent's own row. Nothing a caller sends can name another tenant's run or a root the chain does not have.
The new run's input is the completion payload above.
Read a chain
Both fields are on the run:
GET /v1/automation/portal/runs/{run_id}
→ { "parent_run_guid": "…", "root_run_guid": "…", … }List every run of a chain by filtering on the root:
GET /v1/automation/portal/runs?filters[root_execution_guid]=<root_run_guid>&sort[started_at]=ascThe chain limit
Authoring refuses a self-reference or a loop, but a loop can still form — two bindings created at the same moment, or a row edited outside the API. So a chain also stops at an operator limit.
| Setting | engine.handoffs.limits.max_chain_runs — Maximum runs in one chain under Engine › Workflow hand-offs › Limits |
| Default | 20 |
| Range | 2 to 1000 |
| Scope | platform |
The count is per chain, from the run that started it. When a run completes and its chain already has that many runs, the workflows waiting for it are not started, and the reason is recorded on the completion's handling record, so a redelivery does not try again. A chain of Monitor, Assess and Track is 3 runs.
Step 6 — Watch it, and fix it
Every run is recorded step by step, with the inputs, outputs, errors and the link it followed. You can find runs by the record they are about — "what is happening to this invoice?" — and follow a run to be notified as it progresses.
When something goes wrong, you have verbs beyond "run it again":
| Verb | Use when |
|---|---|
| Restart a step | One step failed and you have fixed the cause |
| Rewind | You need to go back to an earlier step; later steps are marked superseded |
| Replay | You want the whole run again with the same input, recorded as a replay |
| Cancel | The run should stop |
| Signal | The run is waiting and you need to unblock it |
A run whose conditions match nothing stops and waits for an operator rather than guessing. That is by design, and these verbs are how you deal with it.
Step 7 — Turn it down without a deploy
Live controls, while runs are in flight:
- pause, resume or stop a whole connected system, or one action of it
- override the rate limit and the number of concurrent calls per action
- an emergency control to disable every integration at once
Notifications fire on failure, completion, timeout, a stalled run, or a failure rate crossing a threshold — configured at the action, step, workflow or system level, with the most specific winning.
What to rely on
Run state lives in the database, not in a process. Work resumes after a restart, and several workers never double-fire a step. Failed steps retry on a widening backoff up to a limit you set, and a rate-limited step waits without consuming a retry attempt.
Write steps to tolerate being retried. Delivery is at least once.
Where to go next
- Connecting a system to a workflow — turning your APIs into steps.
- Model your first record type — if the records this process runs on do not exist yet.