Workflows
Workflows are graphs of nodes and wires (similar to Node-RED): a run begins at a start node – Start (manual/schedule), HTTP input under /api/v1/flows/<pfad> or collection event – and a message msg (data in msg.payload) travels along the wires. Usage, nodes and examples are described in the manual.
Workflow object
{
"id": 3,
"name": "Bar-Board Karte",
"description": "",
"enabled": true,
"flow": {
"nodes": [
{ "id": "in", "type": "http-in", "name": "", "x": 60, "y": 120, "config": { "method": "GET", "path": "karte", "public": true } },
{ "id": "list", "type": "collection", "name": "", "x": 300, "y": 120, "config": { "action": "list", "collection": "drinks", "onlyPublished": true } },
{ "id": "out", "type": "http-response", "name": "", "x": 540, "y": 120, "config": { "status": 0, "headers": [] } }
],
"wires": [ { "from": "in", "out": 0, "to": "list" }, { "from": "list", "out": 0, "to": "out" } ]
},
"created_at": 1791259204,
"updated_at": 1791259204,
"last_run_at": 1791259233123,
"last_status": "ok",
"next_run": 0
}
| Field | Meaning |
|---|---|
enabled |
Switches HTTP inputs, schedules and events; manual runs always work |
flow.nodes |
id (free, unique), type, name (display), x/y (position in the editor), config per type (see below) |
flow.wires |
Wire from output out (starting at 0) of node from to the input of to. Start nodes have no input |
last_run_at, last_status |
Read-only: last run (milliseconds) and ok/error |
next_run |
Read-only: next scheduled run of all start nodes (Unix seconds), 0 = none |
Node types and config:
type |
Outputs | config |
|---|---|---|
inject |
1 | payload (text, read as JSON if possible), cron (five fields, server time zone or CRON_TZ=…; @every not allowed) |
http-in |
1 | method (GET … DELETE, ANY), path (placeholder {name}, at the end {name...}), public |
event |
1 | collection (slug), created, updated, deleted (bool) |
function |
outputs (1–10) |
code, outputs |
switch |
one per rule | property (path in msg), rules ([{ "op": "eq", "value": "x" }]; op: eq neq lt lte gt gte contains regex true false empty nempty else), all |
http-request |
1 | method, url, headers ([{ "name", "value" }]), body ("" automatic, payload, none, custom with bodyTemplate), ret (auto, json, text), failOnError |
collection |
1 | action (list get create update delete), collection, id (default {{ msg.payload.id }}), onlyPublished, published ("", yes, no) |
illusiondb |
1 | connection (name, see IllusionDB connections), action (list get create update delete schema tables), table (name, Datenquelle/name or ID). list: rules ([{ "col", "op", "val" }]; op: eq neq gt gte lt lte like in not_in is_null is_not_null; in values separated by commas), match (and/or), keepEmpty (don’t omit rules with an empty value), where (filter tree of the IllusionDB API as JSON, replaces rules), sort ([{ "col", "desc" }]), group ([{ "col" }]), summary ([{ "col", "fn" }], fn: SUM COUNT AVG MIN MAX), view (ID of a saved view), limit (≤ 1000), page (starting at 1), all (all pages, ≤ 10,000), fields (comma-separated: load only these columns – ?fields= of the IllusionDB API, also for get; unknown column = error, older versions return all), output (records, full = { records, total, limit, offset, page, pages, has_more, summary, groups }, groups, first). get: field + value (search via one column) or pk (default {{ msg.payload.id }}); not found = null, with failOnMissing an error. update/delete: pk. create/update: columns from msg.payload. cache: seconds for which read results are remembered |
delay |
1 | seconds (0 to 300, fractions allowed) – other branches keep running |
join |
1 | count (0 = as many as incoming wires), mode (array = list of payloads, object = object keyed by msg.topic) |
mail |
1 | to (comma-separated), subject, body (empty = msg.payload), replyTo, html – SMTP access via /system/mail |
http-response |
0 | status (0 = msg.statusCode, otherwise 200), headers |
debug |
0 | property (payload, msg or path) |
custom |
per definition | node (key), params, inputs, outputs, cron – see Custom nodes |
In url, header values, bodyTemplate, id, the fields of mail and of illusiondb (connection, table, pk, value, rule values, limit, page, view; in where as a JSON value or, inside strings, as text), {{ msg.pfad }} is replaced (environment variables are not available in workflows).
GET /workflows/{id}/store ✏️ · DELETE /workflows/{id}/store ✏️
The workflow’s storage (store.get/set in the function node): GET returns [{ "key": "zuletzt", "value": "42", "updated_at": 1791369600 }] (value as JSON text). DELETE ?key=zuletzt deletes one key, without key all of them. Not in the bundle.
GET /workflows 🔑 · GET /workflows/{id} 🔑
All workflows (by name) or a single one.
POST /workflows ✏️ · PUT /workflows/{id} ✏️ · DELETE /workflows/{id} ✏️
Create (201), change, delete (204). PUT only applies the fields sent (the flow always as a whole), supports base_updated_at and first saves a version in the version history (kind=workflow; restoring keeps the name and enabled). 400 for an invalid flow (unknown type, wire to a missing output or into a start node, invalid path or schedule …), 409 if an HTTP input with that path and method already exists in another active workflow.
POST /workflows/{id}/run ✏️
Starts a run and waits for it to finish (time limit 5 minutes). Body optional:
{ "node": "in", "payload": { "monat": "2026-10" }, "flow": { "nodes": [], "wires": [] } }
node is the start node (without it: the first inject), payload replaces its stored value, flow runs an unsaved state (this is how the editor tests). For http-in, an msg.req is created without a real request. Response 200 – even if the run fails:
{
"run": {
"id": 41, "workflow_id": 3, "trigger": "manual", "status": "ok", "error": "", "result": "HTTP 200\n[…]",
"logs": [{ "level": "debug", "node": "dbg", "label": "Debug", "message": "{ … }", "time": 1791259233124 }],
"trace": [{ "node": "in", "time": 1791259233123, "ms": 0, "in": "{…}", "out": ["{…}"] }],
"started_at": 1791259233123, "duration_ms": 2
},
"response": { "status": 200, "headers": { "Content-Type": "application/json; charset=utf-8" }, "body": "[…]" }
}
logs contains debug output, node.warn/console.log and errors; trace contains, per node execution, the input and the messages sent on each output (JSON, truncated; "" = nothing sent). response only if an HTTP response node was reached.
Custom nodes
Reusable script nodes (manual). In the flow: { "type": "custom", "config": { "node": "<key>", "params": { … }, "inputs": 1, "outputs": 2, "cron": "" } } – inputs/outputs reflect the state of the definition (for wires; the admin syncs them when opening), but the current definition is always what runs. inputs: 0 = start node (with cron like inject). Saving a workflow with unknown keys is rejected with 400.
Definition:
{
"id": 3, "key": "preis-formatieren", "name": "Preis formatieren", "description": "…", "color": "#f472b6",
"inputs": 1, "outputs": ["ok", "ohne Preis"],
"params": [{ "name": "waehrung", "label": "Währung", "type": "select", "options": ["€", "$"], "default": "€", "help": "" }],
"code": "…", "created_at": 1791540000, "updated_at": 1791540000
}
type: text, textarea, number, checkbox, select (requires options). name must be a JS identifier (config.name). outputs: 0–10 labels (start nodes at least 1). The code is checked for syntax errors when saving (400).
GET /flow-nodes🔑 ·GET /flow-nodes/{id}🔑POST /flow-nodes✏️ –keyoptional (otherwise derived from the name); taken:409PUT /flow-nodes/{id}✏️ – everything exceptkey; withbase_updated_atconflict check (409). Versioned (kind=flownodein the version history)DELETE /flow-nodes/{id}✏️ –409as long as a workflow uses the nodeGET /flow-nodes/{id}/usage🔑 – names of the workflows that use itPOST /flow-nodes/test✏️ –{ "node": { …Definition, auch ungespeichert }, "params": { … }, "msg": { … } }→{ "status": "ok", "error": "", "outputs": ["<JSON>", ""], "logs": [ … ], "duration_ms": 3 }(the message sent on each output,""= nothing); not logged
Custom nodes are content: table flow_nodes in the bundle.
GET /workflows/{id}/runs 🔑 · DELETE /workflows/{id}/runs ✏️
The last 50 runs of all triggers (manual, cron, http, event, template – call via workflow() from a page), newest first, in the format of run above – or clear the log. Runs are not part of bundles; the workflows themselves are.
HTTP input: /api/v1/flows/{pfad}
Path and method are determined by the HTTP input node. msg.payload is the JSON body (application/json), the form body (application/x-www-form-urlencoded) as an object, otherwise the text – without a body, the query parameters. msg.req contains method, path, params, query (without token), headers (lowercase, without Cookie/Authorization) and body. At most 10 MB, time limit 30 seconds.
| Result | Response |
|---|---|
| HTTP response node reached | its status, headers and msg.payload (object → JSON, text → text/plain) |
| no HTTP response node, no error | 204 |
| error in the flow | 500 with { "error": "workflow “Name”: “Node”: … (line 3, column 7)" } |
Other statuses: 404 (no active workflow for the path), 405 (path matches, method doesn’t), 401/403 for protected inputs (token or admin session; write methods via session need X-IC-CSRF). Public inputs send Access-Control-Allow-Origin: * and answer the OPTIONS preflight.
curl https://example.com/api/v1/flows/karte
curl -X POST https://example.com/api/v1/flows/sync -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"voll": true}'
Collection events
After every successful POST, PUT or DELETE on /collections/{ref}/items…, workflows with a matching event node start in the background (msg.payload = entry, on deletion the state before; msg.event = created/updated/deleted; msg.collection = slug). If workflows trigger further events through their changes, the chain ends after three levels.