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.
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"
}
},
"nodes": [
{
"id": "node-1",
"name": "Check threshold",
"type": "Condition",
"properties": { "expression": "input.value > 90" }
},
{
"id": "node-2",
"name": "Notify",
"type": "Email",
"properties": { "to": "ops@acme.example", "subject": "High temperature" }
}
],
"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 a metric/event |
DeviceUpdate | filter, device, group | A device reports new state |
For the device triggers, filter is device (match properties.device against
the reporting deviceId), group (match properties.group against the device's
group) or unset to match every device in the same scope as the workflow.
Time triggers are validated against node-cron; an invalid expression is
logged and the workflow is simply never scheduled.
Nodes
node.type selects the step implementation:
| Type | Purpose |
|---|---|
Condition | Evaluate an expression and stop or continue |
HTTPCall | Call an external HTTP endpoint |
Email | Send an email |
DeviceAction | Write a device variable, table or command |
CustomFunction | Run a user-supplied JavaScript function |
node.properties is free-form and interpreted by the node implementation. Nodes
execute in array order and each receives the accumulated run data. Workspace
variables are made available to nodes of workspace workflows.
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 document.
Create a workflow
POST /cgg/workflows
{
"name": "Alert on high temperature",
"trigger": { "type": "Webhook", "properties": { "url": "boiler-alert-9f2a" } },
"nodes": [],
"workspace": "664a1f2b8c9d0e0011223344"
}
New workflows are always created disabled (enabled: false) — enable them
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 with $set, so send 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": [
{ "node": "node-1", "output": true },
{ "node": "node-2", "output": { "sent": true } }
],
"createdAt": "2026-08-17T05:58:00.000Z",
"updatedAt": "2026-08-17T05:58:01.000Z"
}
]
}
data is the trigger input, results the per-node output, and completed
indicates whether the chain finished. Runs are sorted newest first.
Workflow runs are deleted automatically after 3 days.
Webhook trigger
ALL /cgg/webhooks/:id
Public endpoint — no session 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 request body is read as plain text
(express.text()), so send Content-Type: text/plain — or any content type — and
parse it inside the workflow if you need structured data.
The workflow receives:
{
"url": "/cgg/webhooks/boiler-alert-9f2a?zone=2",
"method": "POST",
"headers": { "content-type": "text/plain", "…": "…" },
"query": { "zone": "2" },
"body": "raw request body as text"
}
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: text/plain' \
--data 'temperature=93.4'