Control
In the admin: Workflows › a Workflow › a step
Control steps decide where a run goes: which branch, how many times, and how it ends. They touch no data and have no transaction.
| Step | Does | Outputs |
|---|---|---|
| Condition | Branch on a filter | True, False, Error |
| For each item | Run a block once per item of a list | Item, Done, Error |
| Select case | Choose the first matching branch among several | one per case, Default, Error |
| Validate data | Check a value against rules | Success, Error |
| Finish with result | End the run successfully, with a result | none |
| Finish with error | End the run as failed, with a code and a message | none |
Process in batches, a loop over chunks of a list, is with the Composition steps.
Filters in a workflow
Condition, Select case and Validate data evaluate filters in the same grammar as the Data API, over a JSON value in memory rather than a table:
- a path reaches into objects (
customer.email) and a list'slength; - every operator works:
_eq,_neq,_gt,_gte,_lt,_lte,_in,_nin,_between,_like,_ilike,_regex,_null,_nnull,_contains,_contained_by,_overlap,_and,_or,_not; - values are written as text and compared as the field's type:
{"total": {"_gt": "1000"}}compares numbers; $CURRENT_TIMESTAMP,$CURRENT_DATEand$INTERVAL.*are computed by the server, not PostgreSQL:$INTERVAL.1yis 365 days,$CURRENT_DATEis the day in the server's time zone.$USER.*and$PERIOD.*can't be used;- a path the value doesn't have compares as missing, so
_nullholds for it.
A filter value can be a {{…}}.
Condition
| Field | |
|---|---|
| Input | Any JSON value the filter evaluates, typically {{step.data…}} |
| Filter | Evaluated against the input |
True when the filter holds, False otherwise; both are successes. An empty filter is always True. The step's result is its input, unchanged.
Service request intake checks whether a site was found: Input {{find_site.data}}, Filter {"length": {"_eq": "0"}}. True leads to a Finish with error.
For each item
| Field | |
|---|---|
| Items | Must resolve to a JSON array, such as {{read_orders.data}} |
Connect Item to the first step of the loop's body, and Done to what runs after the last item. Inside the body, {{item}} is the current element: {{item.id}}, {{item.site}}.
- Items run one after another, never in parallel.
- Fail-fast: the first item whose body fails stops the loop, which fails through its Error output, or fails the run. Items already done keep their effects.
- A body that ends, at a Finish with result or an empty output, ends that item, not the run.
- An empty list goes straight to Done.
- The result is the list of what each item's body ended with,
{{loop.data}}, with its count in{{loop.meta.count}}.
A list longer than workflows.max_iteration_items (500 by default) fails the step, and every step of every item counts toward a run's step limit, workflows.max_operations_per_run (1,000). Loops can't contain other loops.
Select case
| Field | |
|---|---|
| Input | Any JSON value the cases' filters evaluate |
| Cases | Each a Case name and a Filter, in order. Add case adds one; the arrows reorder them |
| Default branch | Catches any input that matched no case |
The first case whose filter matches wins, and its output is followed. A case name is lowercase letters, digits and underscores, starting with a letter, unique, and not default, error or next; it's the label of its output on the canvas. Each case's filter is required.
With no match and no default branch, the step fails, through its Error output if connected.
Renaming or removing a case removes the steps connected to it.
Validate data
| Field | |
|---|---|
| Input | Any JSON value the rules check |
| Rules | Each a Filter the input must match and a Message reported when it doesn't. Add rule adds one |
Every rule is checked. When all hold, the run continues on Success, with the input as the result. When any fails, the step fails with WORKFLOW_VALIDATION_FAILED and Validation failed: followed by each failed rule's message, separated by ;. A rule without a message reports Rule N failed.
On the Error output, {{error.message}} is that sentence and {{error.detail}} the failed rules, [{ "rule": 1, "message": "…" }].
Service request intake starts with it, on {{start.data.body}}:
| Filter | Message |
|---|---|
{"email": {"_nnull": true}} | An email is required. |
{"description": {"_nnull": true}} | Describe the problem. |
Validate data checks a value in memory, before anything is written. It doesn't replace the table's CHECK constraints or payload checks: PostgreSQL still has the last word when the record is written.
Finish with result
| Field | |
|---|---|
| Result | Any JSON value, with {{…}} |
Ends the run as Succeeded with this result. It's what a Manual run's caller, a synchronous webhook's caller or a calling workflow receives, so it's where you decide what they see: {"workOrder": "{{create_order.data[0].number}}"}. Inside a loop's body, it ends the current item.
A run that ends without one, at an empty output, succeeds with the last step's result.
Finish with error
| Field | |
|---|---|
| Message | The error's message, with {{…}} |
| Code | Optional. A short code callers can test, such as UNKNOWN_CUSTOMER. Without it, WORKFLOW_FINISHED_WITH_ERROR |
Ends the run as Failed with this code and message. Inside a loop's body, it fails the item, and so the loop. It never undoes what earlier steps did, and doesn't choose an HTTP status: a webhook caller still gets 200 with status FAILED.
Next steps
- Values:
{{item}},{{error}}and step results. - Composition: Process in batches, Wait, Execute another workflow.