Workflows
A workflow is a server-side automation: one trigger followed by a chain of nodes. Workflows run in the cloud, independently of the devices, and each execution is recorded as a run. See Workflows for the concepts and the web UI.
Base path: /cgg/workflows (authentication required — API key or session) and
/cgg/webhooks (public).
| Method | Path | Purpose |
|---|---|---|
| GET | /cgg/workflows | List workflows |
| GET | /cgg/workflows/:id | Get one workflow |
| POST | /cgg/workflows | Create a workflow |
| PUT | /cgg/workflows/:id | Update a workflow |
| DELETE | /cgg/workflows/:id | Delete a workflow and its runs |
| GET | /cgg/workflows/:id/runs | List runs of a workflow |
| ALL | /cgg/webhooks/:id | Trigger a webhook workflow (public) |
Access rules
Personal workflows are reachable only by their creator. Workspace workflows are
reachable by the workspace owner, admins and members — not by operators or
tenant-only members, who get
403 {"message":"No access to this workspace workflow"}.
The workflow object
{
"_id": "664d1a2b8c9d0e0011229a01",
"name": "Alert on high temperature",
"description": "",
"enabled": true,
"retrigger": false,
"workspace": "664a1f2b8c9d0e0011223344",
"trigger": {
"type": "DeviceEvent",
"properties": {
"filter": "device",
"device": "665f3c1e9d1b4a0012a7c8d1",
"condition": {
"rules": [
{ "field": "{{ Number(data.event.data.temperature) }}", "operator": ">", "value": "90" }
]
}
}
},
"nodes": [
{
"id": "node-1",
"name": "Notify",
"type": "Email",
"properties": {
"recipient": [{ "email": "ops@acme.example" }],
"subject": "{{ data.device.name }} is overheating",
"body": "<p>{{ data.event.data.temperature }} °C reported.</p>"
}
},
{
"id": "node-2",
"name": "Fan on",
"type": "DeviceAction",
"properties": {
"filter": "device_id",
"device_id": "{{ data.device.deviceId }}",
"action": "set_variable",
"variable": "fan",
"data": "1"
}
}
],
"createdAt": "…",
"updatedAt": "…"
}
Triggers
trigger.type selects how the workflow starts:
| Type | properties | Fires when |
|---|---|---|
Time | schedule — a cron expression | The schedule elapses |
Webhook | url — the webhook path segment | /cgg/webhooks/<url> is called |
DeviceEvent | filter, device, group | A device reports an event (a device log entry of type event) |
DeviceUpdate | filter, device, group | A device's stored state changes — connection, metrics, variables, tables, state or config |
Every trigger type also takes a condition object; see below.
For the device triggers, filter is device (match properties.device against
the reporting deviceId), group (match properties.group against the device's
group) or any to match every device in the same scope as the workflow. A device
belonging to no group is not excluded by a group filter.
Time triggers must be valid standard cron expressions; an invalid expression is
logged and the workflow is simply never scheduled.
Trigger conditions
trigger.properties.condition.rules is evaluated before every run; the workflow
only proceeds if all rules pass. Each rule is a
{ field, operator, value } triple where operator is one of ==, !=, >,
<, >=, <=, and both field and value are
expression strings. An empty rules array always passes.
Comparisons are loose ("30" == 30 is true), and a rule that throws while
evaluating is skipped rather than failing the condition.
condition is effectively requiredThe engine reads trigger.properties.condition.rules without guarding for its
absence. A workflow created over the API with a trigger that has no condition
throws at trigger time, the error is swallowed, and the workflow silently never
runs — no run is recorded. Always send a condition, using
{"rules": []} when you want it to match everything.
Nodes
node.type selects the step implementation:
| Type | Purpose | properties |
|---|---|---|
Condition | Evaluate rules and stop or continue | condition.rules — same shape as a trigger condition |
HTTPCall | Call an external HTTP endpoint | url, method (GET/POST/PUT/DELETE), data, headers — an array of {key, value} |
Email | Send an email | recipient — an array of {email}, subject, body (HTML) |
DeviceAction | Set a device variable or run a command | filter (device/device_id), device or device_id, action (set_variable/execute_command), variable or command, data |
CustomFunction | Run a user-supplied JavaScript function | language (javascript), code — must be synchronous and return its output |
Nodes execute in array order. Each receives the previous node's output as
input, and a Condition whose rules fail ends the run there. node.id is
yours to choose and is what identifies the node in a run's results.
Node output is not checked: an HTTPCall that fails records
{"error": "…"} and the chain continues. Follow it with a Condition on
{{ input.status }} if later nodes depend on it.
Expressions
String properties — on nodes and in condition rules alike — may contain
{{ … }} expressions, evaluated when the workflow runs. Four roots are in
scope:
| Root | Contents |
|---|---|
data | The trigger payload |
input | The previous node's output; empty for the first node |
workspace.variables | Workspace variables, present for workspace workflows |
tenant.variables | Variables of the triggering device's tenant — device triggers only |
A non-string result is inserted as JSON; an expression that throws is left in the string verbatim.
Retrigger
retrigger controls what happens after a run:
| Value | Behaviour |
|---|---|
false (default) | The workflow sets its own enabled to false after the first run that passes the trigger condition, and unschedules itself |
true | The workflow runs every time its trigger fires |
retrigger defaults to false, so a workflow created over the API fires once
and then disables itself. POST ignores the field — set it with a
PUT /cgg/workflows/:id for anything meant to run continuously.
Self-disabling happens before the nodes execute, and — unlike a PUT — is not
written to the audit log.
List workflows
GET /cgg/workflows
| Query parameter | Description |
|---|---|
start | Offset, default 0 |
count | Page size, default 10 |
search | Case-insensitive match on name |
workspace | Return that workspace's workflows instead of personal ones |
Returns the standard { count, data } envelope.
Get one workflow
GET /cgg/workflows/:id
Returns the workflow object.
Create a workflow
POST /cgg/workflows
{
"name": "Alert on high temperature",
"trigger": {
"type": "Webhook",
"properties": { "url": "boiler-alert-9f2a", "condition": { "rules": [] } }
},
"nodes": [],
"workspace": "664a1f2b8c9d0e0011223344"
}
Only name, trigger, nodes and workspace are read from the body. New
workflows are always created disabled (enabled: false) and with
retrigger at its default of false — set both with a PUT once
the nodes are in place. The created workflow is returned.
For Webhook triggers, choose an unguessable url value: the webhook endpoint is
public and the value is the only secret protecting it.
Update a workflow
PUT /cgg/workflows/:id
The body is merged into the workflow, so send only the fields you want to change:
{ "enabled": true }
Notes on behaviour:
_id,userandworkspacein the body are ignored — a workflow cannot be moved between users or workspaces.- If both
enabledandtriggerare absent from the body, the existingtriggeris unset. Always include the trigger when saving a workflow's structure. - Toggling
enabledreschedulesTimetriggers immediately and writes aworkflowaudit log entry.
The response is 200 with an empty body.
Delete a workflow
DELETE /cgg/workflows/:id
Unschedules the workflow and deletes all of its runs along with the workflow itself.
Workflow runs
GET /cgg/workflows/:id/runs
| Query parameter | Description |
|---|---|
start | Offset, default 0 |
count | Page size, default 10, clamped to 1–100 |
{
"count": 214,
"data": [
{
"_id": "664d2b3c8c9d0e0011229a55",
"workflow": "664d1a2b8c9d0e0011229a01",
"completed": true,
"data": { "value": 93.4, "device": "665f3c1e…" },
"results": [
{ "id": "node-1", "output": { "status": "sent" } },
{ "id": "node-2", "output": { "status": 200, "data": { "device": "…", "action": "set_variable", "data": "1" } } }
],
"createdAt": "2026-08-17T05:58:00.000Z",
"updatedAt": "2026-08-17T05:58:01.000Z"
}
]
}
data is the trigger input, results the per-node output keyed by the node's
id, and completed indicates whether the chain finished. Runs are sorted
newest first.
A run is only created once the trigger condition has passed, so a trigger that fires and fails its condition leaves no trace.
Workflow runs are deleted automatically after 3 days.
Webhook trigger
ALL /cgg/webhooks/:id
Public endpoint — no authentication required. :id is matched against
trigger.properties.url of a workflow whose trigger type is Webhook; no match
returns 400.
Any HTTP method is accepted. The shape of body depends on the request's
Content-Type, because the global JSON and form parsers run before this route's
own text parser:
Content-Type | body |
|---|---|
application/json | The parsed object |
application/x-www-form-urlencoded | The parsed object, every value a string |
text/plain | The raw string |
| Anything else, or no body | Empty |
The workflow receives:
{
"url": "/cgg/webhooks/boiler-alert-9f2a?zone=2",
"method": "POST",
"headers": { "content-type": "application/json", "…": "…" },
"query": { "zone": "2" },
"body": { "device": "Boiler 3", "temperature": 93.4 }
}
The response is 200 with an empty body and is sent before the workflow
runs — it acknowledges receipt, not execution. A disabled workflow accepts the
call and does nothing. Check GET /cgg/workflows/:id/runs to see the outcome.
curl -X POST 'https://api.appblocks.io/cgg/webhooks/boiler-alert-9f2a?zone=2' \
-H 'Content-Type: application/json' \
--data '{"device":"Boiler 3","temperature":93.4}'