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.

  1. Fetch the table schema (GET /api/table/<id>/get) for field ids.
  2. Optionally call brick_context / brick_context_preview to discover the tokens, tables, and fields valid at a brick position.
  3. Author the brick tree and validate it (flows/validate) — no persistence, returns the compiled PHPScript or a validation error.
  4. Create the flow (starts inactive).
  5. 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 — key is a field id, value is a token string.
  • record_source_select values 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_records config: {"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"}. table and action are required; filters has the same shape as for query_records. Without group_by it 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 with foreach.
  • foreach config: {"var", "as", "key"}. key is optional and names the loop key variable (the group name when looping a grouped aggregate_records), compiled to foreach ($var as $key => $as). It must be a valid variable name and differ from as.

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"}
    ]
  }
}
  • source is the parent record, in the same pipe-delimited form as elsewhere.
  • subtable is the sub-table's db_table, not its numeric id.
  • update_line and remove_line also take line_id. Get it by looping the parent's lines with get_related in 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.