Skip to main content

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.

The Workflows list

Building a workflow

Create one with + New Workflow, give it a name, then open it. The detail page has three tabs:

TabContents
WorkflowThe canvas — the trigger and the chain of nodes
SettingsEnabled and Retrigger
RunsExecution 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 on the canvas: Device Event, Send Email, Device Action
New workflows are disabled

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

Choosing a trigger

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 * * *" }
A Time trigger set to 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-Typedata.body
application/jsonThe parsed object{{ data.body.temperature }}
application/x-www-form-urlencodedThe parsed object, every value a string
text/plainThe raw string
Anything else, or no bodyEmpty

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.typeFires whenExtra fields
connectionThe device connects or disconnectsstatetrue or false
metricsTelemetry arrivesmetrics — the reported values
variableOne state variable changesname, value
variablesA batch of variables changesvariables
tableA data table changesname
stateDevice state is reportedstate, or name/value
configThe device reports its configurationconfig
{
"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:

FilterMatches
Any DeviceEvery device in the same scope as the workflow
Specific DeviceThe one device you select
Device GroupThe 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.

Ungrouped devices pass a group filter

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.

Device Event trigger filtered to one device, with a condition

Trigger conditions

Every trigger has a Condition made of rules. Each rule compares a field against a value with =, !=, >, <, >= or <=:

FieldOperatorValue
{{ 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.

The five action nodes

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.

PropertyNotes
URLExpressions allowed
MethodGET, POST, PUT or DELETE
DataRequest body. Expressions allowed
HeadersKey/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.

PropertyNotes
ToOne or more recipient addresses
SubjectExpressions allowed
BodySent 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>.

A Send Email node using expressions in its subject and body

Device Action

Writes to a device — the other half of the loop, letting a workflow act on what it observed.

PropertyNotes
Filter TypeSpecific Device to pick one, or Device ID to supply the id
Device IDExpressions allowed — {{ data.device.deviceId }} acts on the device that triggered the run
ActionSet Variable or Execute Command
Variable / CommandThe name to write or invoke
DataThe value or argument. Expressions allowed
A Device Action node setting a variable on the triggering device

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:

RootContents
dataThe trigger payload — its shape depends on the trigger type
inputThe previous node's output; empty for the first node
workspace.variablesWorkspace variables, by name
tenant.variablesTenant 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.

The expression helper listing the available context paths

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.variables is 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.

The Settings tab with Enabled and Retrigger

Retrigger decides what happens after a run:

RetriggerBehaviour
False (default)The workflow disables itself after the first run, and must be re-enabled by hand
TrueThe workflow keeps running every time its trigger fires
This is the most common surprise

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 with
  • results — one entry per node that executed, with the node's id and its output
  • completed — whether the chain reached the end (or was stopped deliberately by a Condition)
A webhook run expanded, showing its trigger payload and node output

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.

Retention

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.

  1. TriggerDevice Event, Filter Type Device Group, group Boilers.
  2. Trigger condition — field {{ Number(data.event.data.temperature) }}, operator >, value 90.
  3. Node 1Send Email. To {{ workspace.variables.ONCALL_EMAIL }}, subject {{ data.device.name }} is overheating, body <p>{{ data.event.data.temperature }} °C reported.</p>.
  4. Node 2Device Action. Filter Type Device ID, Device ID {{ data.device.deviceId }}, action Set Variable, variable fan, data 1.
  5. SettingsRetrigger = True, then Enabled = 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