Workflow definition format
Read the workflow definition format: the trigger, the steps and their types, how steps connect, and the portable export used for import.
A workflow is a JSON document: one trigger, an ordered list of steps, and a few optional fields. You send it to POST /workflows to create a workflow, and you get the same shape back from GET /workflows/:id. Studio builds the same document when you edit on the canvas.
Top-level fields
| Field | Type | Required | Meaning |
|---|---|---|---|
name | string, 1 to 255 characters | Yes | Shown in Studio and in run records |
description | string | No | What the workflow is for |
trigger | object | Yes | What starts a run. See Trigger |
steps | array, at least one | Yes | The steps. See Steps |
inputSchema | JSON Schema object | No | Checks the input of API and Studio runs (see below) |
outputSchema | JSON Schema object | No | Stored with the workflow |
settings | object | No | Run resources. See Settings |
When inputSchema is set, POST /workflows/:id/run checks the required list and the top-level type of each property, and answers 400 with code VALIDATION_ERROR when the input does not match. It does not check nested properties, patterns or ranges.
Trigger
trigger.type picks the trigger. Each type has its own fields; the trigger pages list them.
type | Starts a run when | Reference |
|---|---|---|
api | Your system calls POST /workflows/:id/run | API trigger |
schedule | A cron expression or interval comes due | Schedule trigger |
webhook | An event arrives from a connected provider or on a custom webhook URL | Webhook trigger |
email_event | An email reaches the workflow's own inbound address | Inbound email trigger |
hubspot_event, airwallex_event | A HubSpot or Airwallex event arrives for a connected account | App event triggers |
slack_event, slack_slash_command, slack_interactive | Something happens in a connected Slack workspace | Slack triggers |
The schema also accepts salesforce_event, gmail_event and connection_event. They match events forwarded from those connected accounts and are not covered in these docs.
Whatever its trigger, an active workflow can also be started with POST /workflows/:id/run or with Run in Studio.
Steps
Each step has these fields:
| Field | Type | Required | Meaning |
|---|---|---|---|
id | string | Yes | Unique within the workflow. Later steps read its output as {{steps.<id>.<path>}} |
type | string | Yes | One of the step types below |
name | string | Yes | Shown in Studio and in the run record |
action | string | For type: "action" | <block>.<operation>, for example quickbooks.createInvoice |
config | object | Yes | The step's settings. Values may contain variable references |
errorHandling | object | No | { "strategy": "fail" | "continue" | "retry", "fallback": { … } }. See Errors, retries and timeouts |
terminal | boolean | No | When true, the run ends after this step |
stage | string | No | A stage title shown on the run page (top-level steps only) |
outcome | string | No | A plain-language outcome line shown for this step on the run page |
inputSchema, outputSchema | JSON Schema object | No | When set, the step's resolved config and its output are checked against them; a mismatch fails the step |
provider | string | No | Names the provider the step needs, when it cannot be inferred from action |
retryConfig, timeoutMs | object, number | No | Accepted, but not applied by the run engine today |
Step types
type | What it does | Reference |
|---|---|---|
action | Runs one operation of a block: an integration action, or a core block such as PDF, Stagehand or API Request | Integration actions, Block reference |
condition | Evaluates a condition and runs the step ids in then or else | Condition block |
loop | Runs the listed steps once per element of an array | Loop block |
parallel | Runs branches at the same time | Parallel block |
approval | Pauses the run until a person approves or rejects | Approval block |
wait | Pauses for a duration or until a time | Wait block |
transform | Maps or filters data | Transform block |
agent | Runs an AI Agent step | AI Agent block |
code | Runs JavaScript in a sandbox | Code block |
subworkflow | Runs another workflow | Sub-Workflow block |
data_table | Reads or writes a Loopfour table | Data Table block |
workspace_files | Reads or writes a workspace file | Workspace Files block |
slack, email, gmail | Sends or reads messages through those blocks | Slack, Email |
Integration steps
An integration step uses type: "action" and action: "<block>.<operation>". The block's fields go in config. To run against one account when the company has several for that provider, set config.connection to a connection id from GET /connections; without it the step uses the provider's default connection.
To find the valid blocks, operations and config fields, call GET /blocks and GET /blocks/:type. See Block and action discovery.
How steps run
- Top-level steps run in array order.
- A step listed in a Condition's
thenorelse, a Loop'sstepsor a Parallel branch runs only from there, not again at the top level. - A step with
terminal: trueends the run after it finishes. - The run's
outputis the output of the terminal step when one ended the run. Otherwise it is the output of the last step insteps, or an empty object when that step did not run. - When a step fails and its
errorHandling.strategyis notcontinue, the run stops and its status becomesfailed.
Settings
| Field | Type | Meaning |
|---|---|---|
settings.machine | standard, micro, small-1x, small-2x, medium-1x, medium-2x, large-1x or large-2x | Compute size for each run. standard or no value uses the platform default |
settings.maxDurationSeconds | integer, at least 5 | Wall-clock limit per run. The run is stopped when it is reached |
settings.timeoutMs, settings.maxRetries, settings.concurrencyLimit | number | Accepted, but not applied by the run engine today |
A Sub-Workflow step can override machine and maxDurationSeconds for the child run it starts.
Complete example
A bill check: bills over $10,000 wait for the controller before the record step runs.
{
"name": "Check vendor bill",
"description": "Sends large bills to the controller before recording them",
"trigger": { "type": "api" },
"inputSchema": {
"type": "object",
"required": ["billNumber", "vendor", "amount"],
"properties": {
"billNumber": { "type": "string" },
"vendor": { "type": "string" },
"amount": { "type": "number" }
}
},
"steps": [
{
"id": "check_amount",
"type": "condition",
"name": "Over $10,000?",
"config": {
"conditions": { "left": "{{input.amount}}", "operator": "gt", "right": "10000" },
"then": ["controller_approval"]
}
},
{
"id": "controller_approval",
"type": "approval",
"name": "Controller approves",
"config": {
"approvers": ["controller@example.com"],
"reason": "Bill {{input.billNumber}} from {{input.vendor}} for {{input.amount}}",
"timeout": "3d",
"timeoutAction": "reject"
}
},
{
"id": "record",
"type": "transform",
"name": "Record the bill",
"config": {
"data": {
"billNumber": "{{input.billNumber}}",
"vendor": "{{input.vendor}}",
"amount": "{{input.amount}}"
}
}
}
]
}When the amount is $10,000 or less, the condition is false and controller_approval is skipped. When the controller rejects, or the three days pass, the approval step fails and the run stops before record.
Check a definition before you save it
POST /workflows/validate (scope workflows:read) checks a definition and writes nothing. Send a workflow body or a portable file. The response has valid, plus errors and warnings, each with a code, a message and a JSON path.
Workflow status
| Status | Runs start? | Move to |
|---|---|---|
draft | No | active (POST /workflows/:id/activate), archived |
active | Yes | paused (POST /workflows/:id/pause), archived |
paused | No | active (activate again), archived |
archived | No | draft (POST /workflows/:id/unarchive) |
Activating is what Deploy does in Studio. It checks the steps, publishes the current definition as a new major version (v1.0, v2.0 and so on) when it changed, and sets up the trigger: the schedule for a schedule trigger, the address for an inbound email trigger. Archiving removes the schedule and the inbound address. See the Workflows API for versions and rollback.
Portable export format
To move a workflow between companies, use the portable format. It is the definition with "$schema": "loopfour.workflow-export.v1" added and the company-specific fields (id, status, version, timestamps, creator) removed. It may also carry canvasState, the Studio layout.
{
"$schema": "loopfour.workflow-export.v1",
"name": "Check vendor bill",
"trigger": { "type": "api" },
"steps": [ ... ],
"inputSchema": { ... },
"settings": {}
}The file without $schema and canvasState is a valid POST /workflows body. Connections are not part of the file: after import, connect the systems the steps use in the new company, or the steps that need them fail at run time.
Authentication and API keys
Authenticate API requests with the x-api-key header, choose the scopes a key needs, and create or revoke keys under Settings and API keys.
Variable references
Reference data between steps with template expressions, using the roots that resolve at run time, such as input, steps, secrets and trigger.
Loopfour