#Workflows
A workflow is an ordered set of jobs; each job runs a sequence of actions (HTTP calls, entity reads/writes, file processing, LLM inference, …). Workflows react to triggers — an entity event, a webhook, a cron tick, or a direct dispatch. See Core Concepts for the mental model.
A workflow lives in two layers:
- Definition — the design-time spec: the jobs, actions, and triggers.
Authored under
/definition/*and shareable to other tenants. - Activation — a tenant turning a definition on: its env, trigger secrets,
bound policy, and the runs it produces. Driven under
/workflows/*.
Authenticate with a Personal Access Token (HTTP Basic auth) — enter it once in the Authorize bar at the top of this page and every Try it below uses it. Each example body is pre-filled; the console targets staging by default. Full request/response schemas are in the linked API reference.
#Build a workflow
The flow is: define the workflow → activate it → trigger a run → watch it.
#1. Define the workflow
A workflow binds triggers to job steps. Author each step's logic
inline — embed its actions directly in the step. Inline is the
recommended default: one definition, one version, nothing else to publish or
keep in sync. Each action has a stepId, a name (from the
action catalog), and a config; see
jobs & actions for the conditional fields (if, errorWhen,
errorPolicy, onError). dependencies order steps into a DAG.
POST/definition/workflowsCreate a workflow (inline job)API docs ↗Try it
A definition is draft until the first activation flips it to active. The
write is validated against the workflow JSON schema; per-action config is
validated again at run time, after smart values resolve. Pass source_yaml
instead of definition_json to author in YAML.
Reuse the same actions across workflows? Publish them as a job definition (
POST /definition/jobs) and reference it from a step by name:{ "stepId": "remind", "name": "notify-overdue-task" }. The step names a key, not a version — publishing binds it to your latest active job of that name, and a binding can point it elsewhere per tenant. Reach for a referenced job only when you genuinely need to share one action sequence across several workflows, or to let each environment run a different job behind the same step — otherwise keep it inline.
#2. Activate it
Activation is the consumer tenant turning the definition on: it merges env, reconciles trigger rows, binds an access policy the run acts under, and (for cron) provisions schedules. It is idempotent — re-activating with the same body produces the same state.
POST/workflows/{id}/activateActivate a workflow in your tenantAPI docs ↗Try it
The response returns each trigger with its webhook_url (what external callers
POST to) and config_status. Cron and webhook-secret details are under
Triggers below.
Env is a merge, not a replacement. The definition supplies defaults under
config.env; a tenant's activation overlays its own values on top. Sending a
key sets it, sending it as null unsets it, and a key you do not mention keeps
what it had — so PUT /workflows/{id}/config carrying only job_bindings never
disturbs the env, and one carrying env adds to the defaults rather than
replacing them. Because the definition's values stay live rather than being
copied in, editing the definition reaches every tenant that has not overridden
that key.
Declare configSchemas.env on the definition to name a JSON schema the
effective env must satisfy. It is validated on every config write and again
at dispatch, so a tenant cannot configure a run into a shape the workflow cannot
handle. With a schema declared, required-ness lives in the schema's required
list — a null sentinel in config.env is refused, because the two ways of
saying "this is required" would otherwise disagree. Without a schema, a null
in config.env is still the way to say "the activating tenant must supply
this", and activation fails while any remain unset, naming them under
missing_env_vars.
#3. Trigger a run
Four sources fire a run: an entity event, a webhook POST, a cron
tick, or a direct dispatch. Direct dispatch is the ad-hoc path — pass the
trigger's definition id (e.g. "manual"), not the row UUID:
POST/workflows/wrDispatch a runAPI docs ↗Try it
Returns 202 with the workflow_run.id. Webhook callers instead POST to the
webhook_url from activation; cron and event triggers fire automatically.
#4. Monitor the run
List runs (filter by workflow_id, status_in, entity_id, …), then drill
into one for its per-job and per-action errors.
GET/workflows/runs/workflowsList recent runsAPI docs ↗Try it
GET/workflows/runs/workflows/{id}Inspect a run (job + action detail)API docs ↗Try it
The workflow-run error is null unless an orchestrator-level error fired;
per-step failures live on job_runs[].error (and action_runs[].error).
A run moves pending → running → one of five terminal states: success,
error, cancelled, skipped, or timeout. Poll for membership of that
terminal set rather than for a particular value — skipped is how a run gated
by a concurrency or rerun rule ends, and it is a normal outcome, not a failure.
A run whose step is waiting on an awaitInput callback stays running; the
waiting shows on the action, not on the run.
#Jobs & actions
Inline and referenced jobs run identically — both resolve to the same shape
before execution. Prefer inline (actions directly on the workflow step):
it keeps a workflow self-contained, with one definition to manage. Reach for a
referenced job definition (POST /definition/jobs) when you need to share
one action sequence across several workflows, or to let each environment run a
different job behind the same step.
A reference step names a key, not a job: it carries name and no actions,
and which job that key runs is a separate decision — a binding — made per
definition, per activation, or per integration. Publishing a workflow fills each
unbound name from your own latest active job of that name, so the common case
needs no extra step; pinning something else, or something different per tenant,
is a binding write. See Job Bindings.
#Input and output contracts
A job, and a trigger, may declare the shape it accepts — and a job the shape it produces — as a JSON schema written into the definition:
{
"inputSchema": {
"type": "object",
"properties": { "order_id": { "type": "string" } },
"required": ["order_id"]
},
"outputSchema": {
"type": "object",
"properties": { "sent": { "type": "boolean" } }
},
"actions": [/* … */]
}
An embedded contract travels with the definition, so a job shared to another
tenant validates there without anyone registering a schema first. $ref is
allowed only within the same document — a contract never depends on something
outside it.
The older inputSchemaName / outputSchemaName fields name a registered
catalogue schema instead. They still work, but declare one or the other: a
definition carrying both forms for the same contract is refused at publish, so
there is never a question of which one applied.
#Step-level control (per job step)
A workflow job step accepts a condition gate and error/rerun policies:
| Field | Values | Meaning |
|---|---|---|
dependencies |
string[] |
Step ids that must finish first (defines the DAG). |
condition |
bare JSONata | Evaluated against the step scope below; if false, see conditionPolicy. |
conditionPolicy |
continue | abort | fail |
On false condition: continue skips the step, abort ends the workflow as success, fail ends it as error. Default fail. |
errorPolicy |
continue | abort | fail |
On step error: same three outcomes. Default fail. |
rerunPolicy |
continue | allow | abort |
On a parent retry: reuse prior output, re-run, or end. Default allow. |
#Action-level control (per action, inside a job)
Each action carries optional conditional and recovery fields. This is what lets a single job branch and self-heal without a separate step:
| Field | Values | Meaning |
|---|---|---|
if |
bare JSONata | Evaluated before the action. A falsy result skips the action and its onSuccess and moves on. Mirrors a bridge step's if gate. |
errorWhen |
bare JSONata | Evaluated after the action on its result; a truthy value marks the action failed (for http, defaults to result.status >= 400). |
onSuccess |
action sub-sequence | Actions run after this action succeeds, before the parent sequence continues. |
onError |
action sub-sequence | Recovery actions run after this action fails, before errorPolicy is applied. |
errorPolicy |
fail | continue |
After the action fails (and its onError runs): fail (default) ends the job with the error; continue consumes the error and resumes the parent sequence at the next action. |
Action
errorPolicy(fail/continue) is distinct from the job-steperrorPolicy(continue/abort/fail). The action field governs flow inside a job; the step field governs the workflow outcome when the whole step errors. In the workflow above, thenotifyaction setserrorPolicy: "continue"and anonErrorthat flags the task — so a failed notification is recorded and the job still completes.
Authoring fields like config, body, if, and errorWhen are smart
values: literals, {{ }} JSONata expressions (a whole-string {{ … }}
stringifies), or $-directives for raw values.
Two scopes, not one. A workflow step's fields (input, condition, the
idempotency key) see outputs (earlier steps), context (what the run was
dispatched with — a trigger's event payload arrives as context.input.*), and
config.env. An action's config, inside a job, sees only input and
outputs — a run's context never reaches it. Thread what an action needs
through the step's input and read it as {{ input.x }}. The full model —
bindings, directives, and failure modes — is in
Smart Values.
To reshape a row set inline — filter, map, join, sort, or dedupe a prior
step's output before writing it — wrap it in a { $rows, $pipe } node in any
action config / input / output, or run the shaping as a named pipe
action ({ "rows"?: <array>, "$pipe": [ …steps ] } — rows in, rows out, no
reads or writes; rows defaults to the job's input). The $pipe vocabulary is
the same one apps use; see
Shaping a row set.
#Action catalog
The orchestration layer ships these action types. Every config is a smart
value; see the API reference for each action's exact config schema.
| Action | Kind | Purpose |
|---|---|---|
http |
external | Synchronous HTTP request |
httpCallback |
external | Async HTTP request; resumes on callback |
llmCallback |
external | Async LLM inference (Anthropic / OpenAI) |
awaitInput |
workflow | Pause until an operator submits structured input |
invokeWorkflow |
workflow | Spawn a child workflow run (fire-and-forget) |
queryEntity |
entity | Read one or many entities (filter/sort/one-hop include, keyset cursor, distinctOn, countStrategy) |
queryEntityGraph |
entity | Read a deduplicated entity graph (nested include tree) |
upsertEntity |
entity | Create / update entities (single or bulk) |
pipe |
data | Reshape rows with $pipe steps (pure; no reads/writes) |
removeEntities |
entity | Delete entities by id |
upsertRelationship |
entity | Create / update relationship instances |
removeRelationships |
entity | Delete relationship instances by id |
syncRelationships |
entity | Set-replace one entity's links under a rule |
uploadArtifact / downloadArtifact / listEntityArtifacts / createArtifactDownloadURL |
artifact | Store, fetch, list, and sign artifact access. downloadArtifact with as: "text" reads the body into scope (text, content_type), capped by maxTextBytes |
uploadTempFile / downloadTempFile / createTempFileDownloadURL |
file | Store, read back, and sign access to a temp file — the entity-less counterpart of the three artifact actions |
zipCompress / zipDecompress / archiveDecompress |
file | Compress and extract archives |
splitPdf / mergePdf / chunkPdf |
file | Split, concatenate, and chunk PDFs |
csvFromZipStream / transformCsvStream / mergeCsvStreams |
csv | CSV extract, per-row transform, and join |
importEntitiesFromCsv / exportEntitiesToCsv |
csv | Bulk import/export entities via CSV |
#Triggers
A workflow's definition_json.triggers[] declares when it runs. Each entry
has an id plus exactly one of source (event), cron, or webhook; a
webhook with no verification is the "API-only / manual" form. Triggers are
reconciled on every activate. Optional condition (a bare JSONata gate),
contextData (shapes the run's starting context, which steps read as
context.*), and idempotencyKey apply to any type.
Two behaviors to know. A trigger
conditiongates event triggers only — it's evaluated when the entity event matches. A direct dispatch, webhook, or cron tick is already an explicit request to run, so the condition doesn't gate it. And a source-bound trigger (one with asourceblock) fires only from its entity event: dispatching it directly (or via webhook/cron) is rejected. To re-run over an entity, re-emit its event rather than dispatching the source trigger by hand.
#Event trigger
Fires when an entity is created, updated, or deleted — optionally narrowed to one entity, one relationship rule, or matching attribute values.
{
"id": "on-task-updated",
"source": {
"entityTypeId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"eventAction": "updated", // created | updated | deleted
"relationshipRuleId": "<uuid?>", // optional — only this rule
"entityId": "<uuid?>", // optional — only this entity
"attributeFilters": [
{ "operator": "eq", "path": ["status"], "value": "open" }
]
}
}
#Cron trigger
A standard five-field cron expression, always interpreted in UTC — there is no per-activation timezone, so write the expression in UTC and convert on your side if your operators think in local time.
{ "id": "nightly", "cron": "0 0 * * *" }
Two scheduling behaviours worth knowing. Each schedule carries a random 0–9
second offset, so a fleet of workflows on the same expression does not stampede
the platform on the tick. And a cron that always fires again within the hour
(* * * * *, */5 * * * *) is not retried on a failed tick: the next tick
does the same work, and retrying would queue ticks that later fire together and
create duplicate runs. Hourly and less-frequent crons are retried.
#Webhook trigger
External callers POST to the trigger's webhook_url (returned at activation).
verification checks request authenticity; challenge answers a validation
handshake (e.g. Microsoft Graph) without dispatching a run.
{
"id": "inbound",
"webhook": {
"methods": ["POST"],
"verification": {
"location": "header", // header | body | query
"key": "x-hub-signature-256",
"method": "hmac-sha256" // exact | hmac-sha256 | hmac-sha1
},
"challenge": {
"enabled": true,
"queryParam": "validationToken",
"responseType": "text/plain"
}
}
}
A webhook secret exists only when verification is declared. Mint it (returned
once as whsec_…; rotates any prior secret); the gateway then verifies every
POST /workflows/hooks/:triggerId and rejects a bad signature with 401.
POST/workflows/{id}/triggers/{triggerId}/secretMint a webhook secret (returned once)API docs ↗Try it
After activation, toggle a trigger without re-activating via
PUT /workflows/{id}/triggers/{triggerId}/{enable,disable}.
#Policy & secrets
- Policy — an activation binds an access policy that authorizes everything the run does. See Provisioning.
- Secrets — wire scoped secret refs into
config.env; never put a ref in the definition. See Secrets.
#Inline jobs without a workflow
Apps (bridge) can run the same action sequences inline — synchronously, with no workflow run — for request handling. See Apps (Bridge).
#Share a workflow with another tenant
Publishing a definition for another tenant to install is its own topic — see Share Workflows & Apps (authoring side) and Activate Shared Workflows & Apps (consumer side); the underlying model is in Shares & Grants.
#API reference
Every workflow, job, trigger, and run operation — with full request/response schemas and an interactive console — is in the API reference.