Flows
Flows are per-table automations (triggers). Over the Public API you can list, read, create, update, delete, enable/disable, validate, and inspect the run logs of a table's flows.
All flow endpoints require an account API key (il_live_) with access to
the workspace, and the key acts as a workspace admin — flows are an
admin-level surface. Personal (il_user_) keys cannot author flows. Read-only
keys can read (list, get, validate, get_logs, brick_context) but not
create, update, delete, or set_active.
Authoring modes
A flow's logic lives in its code field, which holds either:
- Builder JSON — the visual builder's brick tree,
{"version":1,"bricks":[…]}. This is the recommended machine-authored format: it round-trips into the visual editor so a human can keep editing it. - PHPScript — raw code (the "code mode" escape hatch).
The format is auto-detected: a code value that JSON-decodes to an object with
a bricks key is treated as builder mode, otherwise as PHPScript.
Triggers
Set on trigger at create time (and changeable via update):
| Value | Trigger | Has record |
|---|---|---|
crt |
On record created | yes |
upd |
On record updated | yes (+ before) |
del |
On record deleted | yes |
dtf |
On date field | yes |
crn |
On schedule (cron) | no |
man |
On demand | no (per-record run) |
web |
On webhook | no |
wpg |
On web page (returns HTML) | no |
cma |
On comment added | yes |
emr |
On email reply | yes |
emo |
On email opened | yes |
web / wpg flows are assigned a public token (webhook_id) automatically.
Schedule settings (crn, dtf)
Clock-based triggers carry their schedule in settings. All times are in the
account owner's timezone.
| Key | Applies to | Value |
|---|---|---|
hour |
crn, dtf |
"0"–"23" (hour of day), or "*" = every hour (crn daily only) |
repeat |
crn |
"daily" (default), "weekly", or "monthly" |
days_of_week |
crn when repeat="weekly" |
array of ISO weekday ints, 1=Mon … 7=Sun (e.g. [1,5] = Mon & Fri) |
days_of_month |
crn when repeat="monthly" |
array of ints 1–31 and/or the string "L" (last day of month), e.g. [1,15] or [1,"L"] |
date_field |
dtf |
date/datetime field id to watch |
offset_days |
dtf |
signed int: +N = fire N days before the date, -N = after, 0 = on |
The server normalizes schedule settings on save: out-of-range values are
dropped; if the active mode's day list is empty it collapses to "daily"; and
because "Every Hour" is daily-only, hour="*" is coerced to "9" when repeat
is weekly or monthly.
Runtime setting (all triggers)
| Key | Value |
|---|---|
max_runtime |
int seconds a single run may spend working. Omit (or 0) for the default of 30s. |
Two limits apply to an automated run. Working time is capped at 30 seconds by default (runaway protection for unattended runs), and total elapsed time, waiting included, is capped at 300 seconds. Time spent blocked on an outbound call does not count towards the working-time limit, so a flow calling a slow API or an AI model needs no setting at all.
Raise max_runtime only for flows that do heavy work themselves, such as looping
over thousands of records. It raises both limits: total time is always at least
max(300, max_runtime) seconds.
The value is clamped silently on save, never rejected: to the account plan's
ceiling (300s / 450s / 600s by tier), and for wpg (web page) flows to 55s,
because a browser is blocked on the response. A downgraded plan therefore never
makes an existing flow unsavable. Read the effective value back with
GET /api/table/<id>/flow/<flowId>/get.
Manual (man) flows run with no time limit when triggered interactively from a
button or bulk action — they are watched and can be stopped by the user.
Outbound calls are additionally clamped to the run's remaining budget, so a
step asking for a longer timeout than the flow can survive is cut short rather
than run to completion and discarded. See the timeout option on the
http_request / api_request bricks.
Recommended workflow
- Fetch the table schema (
GET /api/table/<id>/get) for field ids. - Optionally call
brick_context/brick_context_previewto discover the tokens, tables, and fields valid at a brick position. - Author the brick tree and validate it (
flows/validate) — no persistence, returns the compiled PHPScript or a validation error. - Create the flow (starts inactive).
- Enable it with
set_active.
List flows for a table
GET /api/table/<table_id>/flows/list
Returns id / name / trigger / active / err_flag for every flow on the table
(not the code). Use get to read a single flow's logic.
Get a single flow
GET /api/table/<table_id>/flow/<flow_id>/get
Returns the full flow including code (builder JSON or PHPScript), trigger,
settings, active, and err_flag. This is the read-before-edit endpoint.
Validate (dry run)
POST /api/table/<table_id>/flows/validate
Body {"code": "<builder JSON or PHPScript>"}. Does not persist. Builder
JSON is validated against the table schema (unknown field ids / tokens are
rejected) and compiled; PHPScript is linted.
// builder mode
{"ok": true, "mode": "builder", "compiled": "…compiled PHPScript…"}
// code mode
{"ok": true, "mode": "code"}
On failure the call returns a non-2xx with the validation / lint message.
Create a flow
POST /api/table/<table_id>/flows/create
{
"trigger": "crt",
"data": {
"name": "Stamp created date",
"code": "{\"version\":1,\"bricks\":[…]}",
"active": false
}
}
trigger is required. data is optional; supported keys: name, code,
active, settings. New flows default to inactive — enable with
set_active after you've confirmed the logic. Returns the created flow.
When the code is builder JSON, it is validated against the table schema on
save; un-compilable JSON is rejected with the validation error rather than
persisted.
Update a flow
POST /api/table/<table_id>/flow/<flow_id>/update
{
"data": {
"id": 456,
"name": "Renamed",
"code": "{\"version\":1,\"bricks\":[…]}",
"trigger": "upd",
"settings": {}
}
}
data.id and data.name are required. id, table_id, and account_id are
ignored if sent (the path is authoritative). Changing trigger to web/wpg
auto-generates a public token if the flow doesn't have one. Builder code is
schema-validated on save. Returns the updated flow.
Enable / disable
POST /api/table/<table_id>/flow/<flow_id>/set_active
Body: {"active": true} enables, {"active": false} disables. 1/0 and
"true"/"false" work too. Blocked for read-only keys.
If you omit the active key entirely, the legacy rule applies: any non-empty body
enables, an empty body ({}) disables. Prefer the explicit form.
Returns a summary of the flow, not the whole thing: id, table_id, name,
trigger, active. Use update if you need the full flow back.
Delete a flow
POST /api/table/<table_id>/flow/<flow_id>/delete
Run logs
POST /api/table/<table_id>/flow/<flow_id>/get_logs
Body {"offset": 0} (optional). Returns a page of run-log rows for debugging
a flow's executions.
Brick context (authoring discovery)
POST /api/table/<table_id>/flow/<flow_id>/brick_context # existing flow
POST /api/table/<table_id>/flows/brick_context_preview # unsaved / new flow
Body: {"bricks": {"bricks":[…]}, "brickId": "<target brick id>"} (preview
also takes trigger, and both accept additional_workspaces). Returns the
variables, {{tokens}}, tables, select options, multi-value fields, and
workspace users valid at that brick's position — use these to author valid
bricks before saving.
Builder JSON
Each brick is {"id","type","config"}, with container bricks (if,
if_expr, foreach) nesting under children (and else_children for if /
if_expr). Minimal example — on an upd flow, set a checkbox when a number
crosses a threshold:
{
"version": 1,
"bricks": [
{
"id": "b1",
"type": "if",
"config": {"left": "{{record.number}}", "operator": ">", "right": "250"},
"children": [
{
"id": "b2",
"type": "update_record",
"config": {
"source": "object|record|<db_table>",
"fields": [{"key": "single-check", "value": "bar"}]
}
}
]
}
]
}
Notes:
config.fields(create/update) is an array of{key, value}objects —keyis a field id,valueis a token string.record_source_selectvalues are pipe-delimited:object|record|<db_table>for the trigger record,object|<varName>|<db_table>for an object variable.- Date / datetime / time fields are stored in UTC — stamp them with
{{now.datetime_utc}}/{{now.date_utc}}/{{now.time_utc}}, not the local{{now.datetime}}. - Available brick types include
set_var,html_var,web_page,foreach,if,if_expr,query_records,get_records_from_view,get_record,get_related,get_record_tasks,aggregate_records,create_record,update_record,delete_record,add_line,update_line,remove_line,add_comment,send_email,generate_pdf,http_request,api_request,ai_prompt,assign_task,execute_flow,stop,continue,log,comment. aggregate_recordsconfig:{"table", "action": "count|sum|avg|min|max", "field", "filters", "group_by", "group_date": "day|week|month|year", "sort": "value_desc|value_asc|label_asc|label_desc", "var"}.tableandactionare required;filtershas the same shape as forquery_records. Withoutgroup_byit saves one number; with it, a map of group name to number ({"Won": 150, "New": 30}). Read one group as{{total.Won}}, or loop it withforeach.foreachconfig:{"var", "as", "key"}.keyis optional and names the loop key variable (the group name when looping a groupedaggregate_records), compiled toforeach ($var as $key => $as). It must be a valid variable name and differ fromas.
Line items (master-detail)
A sub-table row exists only inside its parent, so a flow writes one through the parent with add_line, update_line or remove_line. Pointing create_record, update_record or delete_record at a sub-table is rejected when the flow is saved.
{
"id": "b1",
"type": "add_line",
"config": {
"source": "object|record|invoices",
"subtable": "invoice_lines",
"fields": [
{"key": "label", "value": "Install"},
{"key": "qty", "value": "2"}
]
}
}
sourceis the parent record, in the same pipe-delimited form as elsewhere.subtableis the sub-table'sdb_table, not its numeric id.update_lineandremove_linealso takeline_id. Get it by looping the parent's lines withget_relatedin the incoming direction ("table": "in:invoice_lines") and reading{{line.id}}.- Each of these steps returns the parent as written, and the flow's record variable is updated to match, so a later step reads the recomputed rollup totals. The new line's own id is not returned.
Always validate a brick tree against the target table before saving — the
validator resolves field ids and tokens against the table schema and returns a
precise error for anything it can't compile.
Errors
| Status | Message | When |
|---|---|---|
| 403 | API endpoint not available to API keys |
Method not in the account-key allowlist (e.g. compile_bricks). |
| 403 | Endpoint not available to personal API keys |
A personal (il_user_) key called a flow endpoint. |
| 403 | API key is read-only |
Read-only key called create / update / delete / set_active. |
| 500 | API key not authorized for this workspace |
Key's space whitelist excludes the table's workspace. |
| 500 | Not Authorized |
Key's user is not a workspace admin. |
| 500 | Invalid Flow |
flow_id doesn't exist on the table. |
| 500 | validation message | Builder JSON failed schema validation or PHPScript failed to lint. |