The whole DSL is one call to workflow({ … }). This page is the quick reference; the guides go
deeper on each construct.
The workflow() object
| Key | Type | Purpose |
|---|---|---|
name | string | Required. The definition name you start runs against. |
version | string | Optional display label. |
description | string | Optional. |
initial | object | Starting variables; --input is merged over this at start. |
inputSchema | JSON Schema | Optional; validates trigger --input. |
triggers | array | Required. One or more triggers (see below). |
steps | array | Required. Step builders; exactly one has start: true. |
result | (vars) => object | Optional terminal reducer for queryable output. |
outcomeTemplates | object | Mustache strings for run-history rendering. |
inboxTemplates | object | Mustache {title, subtitle} for HITL inbox cards. |
Step builders
| Builder | Signature | Notes |
|---|---|---|
step | step(name, opts, fn) | Regular node. fn is always required, even for pure routers. |
form | form(name, opts, fn) | Parks WAITING_FORM; fn(ctx, data). |
signal | signal(name, opts, fn) | Parks WAITING_SIGNAL; fn(ctx, data). |
webhook | webhook(name, opts, fn) | Parks WAITING_WEBHOOK; fn(ctx, data). |
connector | connector(name, opts) | External call; no positional handler. |
agent | agent(name, opts, fn) | Parks WAITING_AGENT; asks the driving agent; fn(ctx, data). |
query | query(name, opts, fn) | Read-only; not part of the flow graph. |
The handler contract: function(ctx) { return { … } } for steps; function(ctx, data) { … }
for form/signal/webhook/agent, where data is the inbound payload. The returned object is
merged into ctx.vars.
Step options (opts)
| Option | Type | Meaning |
|---|---|---|
start | bool | The single entry step. |
next | string | Next step name, or "end". |
label | string | Display name for UI / narrative. |
branches | array | [branch(cond, target), …] — first true condition wins. |
loop | object | { condition, next, breakNext } (while-loop; next is the body). |
foreach | object | { collection, itemVar, body, next } — iterate an array variable. |
parallel | object | { steps: [names…], next } — run concurrently, then join. |
onError | string | Step to route to when the handler throws (ctx.error is populated). |
retryPolicy | object | { maxAttempts, initialInterval, backoffCoefficient, maximumInterval }. |
narrative | function | fn(ctx, status, branch) → human-readable status text. |
Conditions (branches & loops)
Expressions over the current variables:
- Comparisons:
==,!=,>,<,>=,<= - Connectives:
AND,OR - Literals:
true,false, numbers,'single-quoted strings' - Catch-all: list
branch("true", "fallback")last
branches: [
branch("total > 1000 AND status != 'pending'", "manual_review"),
branch("payment_status == 'success'", "fulfill"),
branch("true", "reject"),
]
Triggers
| Builder | Starts a run via |
|---|---|
apiTrigger(name) | flowctl run start --workflow <name> |
formTrigger(name) | Submitting a trigger form |
webhookTrigger(name) | HTTP POST to the trigger URL |
scheduleTrigger(name, { cron, collision, timezone }) | A cron schedule |
scheduleTrigger cron is 5-field, minute-granular. collision is "skip" (default),
"queue", or "parallel".
Under the default skip collision, a scheduled run that parks on a form/agent can
block later fires until a human responds. Decouple the human leg, or use queue /
parallel. Keep an apiTrigger for manual testing.
Connector adapters
requestMapper(ctx) returns the request; responseCombiner(ctx, response) reads the result —
and the envelope differs per adapter. Always confirm shapes with
flowctl conn schema <connection> <operation>.
| Adapter | requestMapper returns | responseCombiner sees |
|---|---|---|
generic-http | { body, path_params?, query_params? } | { status_code, body, headers } |
llm (OpenRouter) | { model, messages: [{ role, content }] } | { choices, usage, … } |
composio | flat action args, e.g. { recipient_email, subject, body } | { data, successful, error } — check successful |
mcp | the MCP tool’s argument object | the tool’s result payload |
Control directives (returned from handlers)
| Call | Effect |
|---|---|
action.transition("step") | Jump to a named step (overrides next). |
action.end() | End the run now (COMPLETED). |
action.fail("reason") | Marks the run FAILED — only takes effect on a terminal step (one whose flow resolves to "end"). To fail mid-flow, route to a terminal failing step via onError / a branch. |
action.log("msg") is accepted for compatibility but is currently a no-op (nothing is
emitted or persisted) — use it sparingly, and don’t rely on it for diagnostics yet.
Driving a run with flowctl
# Definitions
flowctl def create --file <workflow.js> # register; prints id
flowctl def list
# Runs
flowctl run start (--workflow <name> | --def <id>) --input '<json>' [--channel <id>]
flowctl run status <exec> -o json
flowctl run watch <exec>
flowctl run cancel <exec>
# Resume parked runs
flowctl form submit <exec> <formName> --data '<json>'
flowctl signal send <exec> --name <signalName> --data '<json>'
flowctl webhook call <exec> <path> --payload '<json>'
flowctl agent submit <exec> <name> --type json --result '<json>'
# Read-only
flowctl query run <exec> <queryName>