Skip to main content

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).

MethodPathPurpose
GET/cgg/workflowsList workflows
GET/cgg/workflows/:idGet one workflow
POST/cgg/workflowsCreate a workflow
PUT/cgg/workflows/:idUpdate a workflow
DELETE/cgg/workflows/:idDelete a workflow and its runs
GET/cgg/workflows/:id/runsList runs of a workflow
ALL/cgg/webhooks/:idTrigger 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:

TypepropertiesFires when
Timeschedule — a cron expressionThe schedule elapses
Webhookurl — the webhook path segment/cgg/webhooks/<url> is called
DeviceEventfilter, device, groupA device reports an event (a device log entry of type event)
DeviceUpdatefilter, device, groupA 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 required

The 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:

TypePurposeproperties
ConditionEvaluate rules and stop or continuecondition.rules — same shape as a trigger condition
HTTPCallCall an external HTTP endpointurl, method (GET/POST/PUT/DELETE), data, headers — an array of {key, value}
EmailSend an emailrecipient — an array of {email}, subject, body (HTML)
DeviceActionSet a device variable or run a commandfilter (device/device_id), device or device_id, action (set_variable/execute_command), variable or command, data
CustomFunctionRun a user-supplied JavaScript functionlanguage (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:

RootContents
dataThe trigger payload
inputThe previous node's output; empty for the first node
workspace.variablesWorkspace variables, present for workspace workflows
tenant.variablesVariables 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:

ValueBehaviour
false (default)The workflow sets its own enabled to false after the first run that passes the trigger condition, and unschedules itself
trueThe workflow runs every time its trigger fires
caution

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 parameterDescription
startOffset, default 0
countPage size, default 10
searchCase-insensitive match on name
workspaceReturn 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, user and workspace in the body are ignored — a workflow cannot be moved between users or workspaces.
  • If both enabled and trigger are absent from the body, the existing trigger is unset. Always include the trigger when saving a workflow's structure.
  • Toggling enabled reschedules Time triggers immediately and writes a workflow audit 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 parameterDescription
startOffset, default 0
countPage 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.

Retention

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-Typebody
application/jsonThe parsed object
application/x-www-form-urlencodedThe parsed object, every value a string
text/plainThe raw string
Anything else, or no bodyEmpty

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}'