Workflows
A workflow is a server-side automation that runs in AppBlocks Cloud rather than on a device. It is made of exactly one trigger and an ordered chain of action nodes:
Because workflows run in the cloud, they keep working when a device has no logic for the case at hand, they can reach several devices at once, and they can talk to systems the device itself cannot — email, an external HTTP API, your own JavaScript.
Every execution is recorded as a run, so you can see what a workflow received and what each node returned.
Workflows live in the sidebar under Workflows. Like every other piece of cloud content they belong either to your personal space or to a workspace — a workspace workflow is visible to the owner, admins and members of that workspace.

Building a workflow
Create one with + New Workflow, give it a name, then open it. The detail page has three tabs:
| Tab | Contents |
|---|---|
| Workflow | The canvas — the trigger and the chain of nodes |
| Settings | Enabled and Retrigger |
| Runs | Execution history |
The canvas opens read-only. Click Edit to make changes, and Save when you are done.
Start by clicking + Set Trigger and picking one, then + Add Action to append nodes. Selecting any element opens its properties panel on the right.

A workflow is created with Enabled = False and stays that way until you change
it on the Settings tab. Build the chain first, then enable it.
Triggers
The trigger decides when the workflow starts and what data it receives. The
payload is available to every node as data (see
Expressions).

Time
Runs on a cron schedule — five fields, minute hour day month weekday. The
default 0 * * * * is every hour, on the hour.
*/15 * * * * every 15 minutes
0 6 * * * 06:00 every day
0 8 * * 1 08:00 every Monday
An invalid expression is not rejected when you save it — the workflow is simply never scheduled, so double-check the syntax.
The payload is the scheduled time and the expression that produced it:
{ "time": "2026-09-11T06:00:00.000Z", "schedule": "0 6 * * *" }

Webhook Called
Runs when an HTTP request arrives at a generated URL. The URL is read-only — AppBlocks assigns it — and the endpoint is public, so treat the URL itself as the secret that protects the workflow.
curl -X POST 'https://api.appblocks.io/cgg/webhooks/<generated-id>?zone=2' \
-H 'Content-Type: application/json' \
--data '{"device":"Boiler 3","temperature":93.4}'
Any HTTP method is accepted. How data.body arrives depends on the request's
Content-Type:
Content-Type | data.body |
|---|---|
application/json | The parsed object — {{ data.body.temperature }} |
application/x-www-form-urlencoded | The parsed object, every value a string |
text/plain | The raw string |
| Anything else, or no body | Empty |
So for the JSON call above:
{
"url": "/cgg/webhooks/<generated-id>?zone=2",
"method": "POST",
"headers": { "content-type": "application/json", "…": "…" },
"query": { "zone": "2" },
"body": { "device": "Boiler 3", "temperature": 93.4 }
}
Send text/plain and the same field needs
{{ JSON.parse(data.body).temperature }} instead. If you do not control the
caller, handle both:
const r = typeof data.body === 'string' ? JSON.parse(data.body) : data.body;
The caller gets 200 immediately, before the workflow runs — the response
acknowledges receipt, not execution, and says nothing about whether the workflow
succeeded or was even enabled. Check the Runs tab for the outcome.
Device Event
Runs when a device reports an event — the entries that show up in the device
log with a type of event, including the device coming online. Debug output does
not trigger workflows.
{
"device": { "deviceId": "…", "name": "Boiler 3", "…": "…" },
"event": { "type": "event", "data": { "temperature": 93.4 } }
}
Device Update
Runs when a device's stored state changes in the cloud. This is the broader of
the two device triggers; update.type tells you what happened:
update.type | Fires when | Extra fields |
|---|---|---|
connection | The device connects or disconnects | state — true or false |
metrics | Telemetry arrives | metrics — the reported values |
variable | One state variable changes | name, value |
variables | A batch of variables changes | variables |
table | A data table changes | name |
state | Device state is reported | state, or name/value |
config | The device reports its configuration | config |
{
"device": { "deviceId": "…", "name": "Boiler 3", "…": "…" },
"update": { "type": "variable", "name": "temperature", "value": "93.4" }
}
Which devices a device trigger watches
Both device triggers take a Filter Type:
| Filter | Matches |
|---|---|
| Any Device | Every device in the same scope as the workflow |
| Specific Device | The one device you select |
| Device Group | The devices in the group you select |
Scope always applies on top of the filter: a workspace workflow only ever sees that workspace's devices, and a personal workflow only your own.
A Device Group filter excludes devices that belong to a different group,
but a device that belongs to no group is not filtered out. If your workspace
mixes grouped and ungrouped devices, add a trigger condition on
data.device.deviceId — or put every device in a group.

Trigger conditions
Every trigger has a Condition made of rules. Each rule compares a field
against a value with =, !=, >, <, >= or <=:
| Field | Operator | Value |
|---|---|---|
{{ data.update.value }} | > | 30 |
{{ data.device.name }} | != | Test rig |
Both sides are expressions, so either can be a literal or computed. All rules must pass for the workflow to run — they combine with AND — and a condition with no rules always passes.
Comparisons are loose: "30" == 30 is true. > and < on two strings compare
them alphabetically, so wrap a numeric field in Number(...) when the device
reports it as text:
{{ Number(data.update.value) }} > 30
A rule whose field or value fails to evaluate is skipped rather than treated as false, so a typo in a path can let a workflow through. Check the Runs tab if a condition seems to be ignored.
Nodes
Nodes run top to bottom. Each one receives the previous node's output as input
— for the first node, input is empty — and its own output is recorded in the
run.

Condition
Gates the rest of the chain. Same rules and operators as a trigger condition; if they pass, execution continues, and if not the run stops here and is marked completed. Use it to branch on something a previous node returned, which the trigger condition cannot see.
HTTP Request
Calls an external endpoint.
| Property | Notes |
|---|---|
| URL | Expressions allowed |
| Method | GET, POST, PUT or DELETE |
| Data | Request body. Expressions allowed |
| Headers | Key/value list |
Content-Type defaults to text/plain, but if the evaluated body parses as JSON
it is sent as application/json instead. Setting the header yourself always
wins.
The output is { "status": …, "data": … }, or { "error": "…" } if the request
failed — which does not stop the chain. Follow the call with a Condition
on {{ input.status }} if later nodes depend on it having worked.
Send Email
Sends an email from automation@appblocks.io.
| Property | Notes |
|---|---|
| To | One or more recipient addresses |
| Subject | Expressions allowed |
| Body | Sent as HTML. Expressions allowed |
<p>Boiler 3 reported {{ data.update.value }} °C at {{ new Date().toISOString() }}.</p>
Because the body is HTML, plain newlines are not line breaks — use <br/> or
<p>.

Device Action
Writes to a device — the other half of the loop, letting a workflow act on what it observed.
| Property | Notes |
|---|---|
| Filter Type | Specific Device to pick one, or Device ID to supply the id |
| Device ID | Expressions allowed — {{ data.device.deviceId }} acts on the device that triggered the run |
| Action | Set Variable or Execute Command |
| Variable / Command | The name to write or invoke |
| Data | The value or argument. Expressions allowed |

The node reports success once the message has been handed to the device channel. It does not wait for the device to acknowledge it, so a device that is offline produces a successful-looking run — check the device's own state afterwards if that matters.
Custom Function
Runs JavaScript when no other node fits.
const c = Number(data.update.value);
return { fahrenheit: c * 9 / 5 + 32, hot: c > 30 };
data, input, workspace and tenant are in scope as ordinary values, and
whatever you return becomes the node's output — so the example above lets the
next node use {{ input.fahrenheit }}.
The code must be synchronous: there is no await, no fetch and no timers.
Use an HTTP Request node for anything that needs to leave the server. If the
function throws, the output is empty and the chain continues.
Expressions
Any text field accepts {{ }}, evaluated when the workflow runs:
Hello {{ data.device.name }}
{{ data.event.data.temperature > 30 ? "hot" : "ok" }}
Full JavaScript works inside the braces. For dropdown, toggle and table fields, click the fx button beside the field label to bind the field to an expression instead of a fixed value.
Four roots are available:
| Root | Contents |
|---|---|
data | The trigger payload — its shape depends on the trigger type |
input | The previous node's output; empty for the first node |
workspace.variables | Workspace variables, by name |
tenant.variables | Tenant variables of the device's tenant |
{{ workspace.variables.API_ENDPOINT }}
The properties panel has an expression helper — the ? button in its header — that lists every path the current trigger and node actually offer. Use it rather than guessing at a payload shape.

Two things worth knowing:
- A value that is not a string is inserted as JSON, so an object becomes
{"a":1}rather than[object Object]. - An expression that throws is left in the text as written — seeing a literal
{{ … }}in a run or an email is the sign of a bad path. tenant.variablesis only populated for the device triggers, and only when the device belongs to a tenant.
Enabled and Retrigger
Both live on the Settings tab, and together they decide how long a workflow keeps working.
Enabled switches the workflow on and off. Disabling it unschedules a Time
trigger immediately; a webhook call to a disabled workflow is still accepted but
does nothing.

Retrigger decides what happens after a run:
| Retrigger | Behaviour |
|---|---|
False (default) | The workflow disables itself after the first run, and must be re-enabled by hand |
True | The workflow keeps running every time its trigger fires |
A new workflow defaults to Retrigger = False, which means it fires once and
then turns itself off. If a workflow worked the first time and never again, this
is why. Set Retrigger = True for anything meant to run continuously — a
scheduled report, an alert on a metric, a webhook endpoint.
A one-shot workflow is occasionally what you want — a migration, a single broadcast to a fleet — but it is rarely the default you want for monitoring.
Turning a workflow on or off is recorded in the workspace audit log.
Runs
The Runs tab lists executions newest first, each expandable to the full record:
data— the trigger payload the run started withresults— one entry per node that executed, with the node'sidand itsoutputcompleted— whether the chain reached the end (or was stopped deliberately by a Condition)

A run only appears if the trigger fired and the trigger condition passed. If you expect a run and there is none, the condition is the first thing to check; if the run is there but short, look at which node's output is missing.
Runs are deleted automatically after 3 days.
Worked example
Email the on-call address when a boiler reports over 90 °C, and turn its fan on.
- Trigger — Device Event, Filter Type Device Group, group
Boilers. - Trigger condition — field
{{ Number(data.event.data.temperature) }}, operator>, value90. - Node 1 — Send Email. To
{{ workspace.variables.ONCALL_EMAIL }}, subject{{ data.device.name }} is overheating, body<p>{{ data.event.data.temperature }} °C reported.</p>. - Node 2 — Device Action. Filter Type Device ID, Device ID
{{ data.device.deviceId }}, action Set Variable, variablefan, data1. - Settings —
Retrigger = True, thenEnabled = True.
Keeping the threshold in the trigger condition rather than in a Condition node matters: a trigger condition that fails records no run at all, so the Runs tab stays readable instead of filling with no-ops on every normal reading.
See also
- Workflows HTTP API — manage workflows programmatically
- Workspaces, Tenants & Roles — scoping, variables and who can edit
- Scheduler feature — on-device scheduling, for logic that must run without a network