Flows
Flows are a visual way to wire up real-time rules, alerts, and automations in Control Seat. You drag triggers, logic, and actions onto a canvas, connect them with arrows, and publish — no code required for most tasks. Think "when this tag goes out of range, do that" or "every morning at 6am, fetch this and post it to Slack."
What Is A Flow?
A flow is a graph of nodes connected by edges. A message enters through a trigger, flows through logic and transformations, and ends at an action (or nothing — some flows just write a tag and stop).
Flows run on the gateway, not in the browser. That means they keep working after you close the tab: schedules still fire, webhooks still respond, alarms still notify.
Common things people build with flows:
- Email ops when pump temperature stays above 180°F for 30 seconds
- Write a daily setpoint at 6am based on the previous day's production total
- POST every active alarm to a Slack webhook
- On any rising edge of a digital input, increment a counter tag
- Ask an LLM to summarize the last hour of production and save the result
- Flag anomalies early using a rolling z-score or trend detector, even without a fixed threshold
Core Concepts
Nodes, Edges, Messages
Nodes do things. Each node has one job — read a tag, check a threshold, send an email.
Edges carry messages between nodes. You draw them by clicking an output handle on one node and dragging to the input handle of another.
A message is the data that travels along an edge:
{
"payload": { "value": 187.3 },
"context": {},
"source": { "nodeId": "n_trigger", "handle": "out" }
}
payloadis your data — a number, a string, an object. You reference it from any node's config asmsg.payloador justpayload.contextis a scratch bag shared by every node in a single flow run.sourcetells you which upstream node just fired you. Useful when multiple edges converge on one node (see fan-in).
Triggers And Actions
Every flow starts at a trigger. Triggers have no input handle — they produce messages in response to something happening (a tag changed, a schedule fired, a webhook arrived).
Most flows end at an action — a node with side effects out in the world (sending email, making an HTTP call, writing a tag).
Handles
Most nodes have one input (in) and one or more outputs. Common output handles:
out— normal flowerror— fired if the node throws or fails validation (amber dot on the canvas)true/false— Branch node routes here based on the conditioncase_0/case_1/ … — Switch node, one per case rowdropped— Throttle node, for messages rejected inside the rate-limit windowanomaly/alarm— AI detector nodes use these for "something looks off" events, in parallel withoutreset— Join and Debounce accept a reset input; send anything to it to wipe buffered state
If nothing is wired to a handle, messages sent there are silently dropped. An unwired error handle means the flow just keeps going when that node fails — wire it up to catch real problems.
Getting Started
Step 1: Open The Flows Panel
In the left sidebar of the editor, switch from Pages to Flows using the mode toggle at the top of the panel. The Flows panel lists every flow in your project, grouped into folders.
Step 2: Create A Flow
Click New Flow. Give it a name (something like "Pump temp alarm"). An empty canvas opens in the main editor area.
Step 3: Add A Trigger
Drag a trigger from the Node Library popover onto the canvas. Shortcut: drag any tag from the Tags panel straight onto the canvas and a Tag Change trigger is auto-created and wired to that tag.
Step 4: Add Logic And Actions
Drag more nodes onto the canvas. Connect them by clicking an output handle (circles on the right side of each node) and dragging an edge to an input handle (left side).
Keep the layout loose — flow top-left to bottom-right is conventional but not required.
Step 5: Configure Each Node
Click any node. The right-side inspector shows its settings. Fill in tag paths, expressions, URLs, thresholds. Required fields are marked. Read the inline help on each field — it explains what's expected and often shows an example.
Step 6: Test It
Drop a Manual Run trigger on the canvas and connect it to the start of your flow (in parallel with the real trigger). Select the Manual Run node — the inspector shows a JSON Payload editor and a Run Now button. Click it to fire the flow end-to-end with a payload you control.
Every edge shows live payload data as messages flow. Click any edge to see the full last payload in the right-side inspector, along with copyable reference paths like msg.payload.value.
Step 7: Publish
When the flow behaves the way you want, open the Publish popover and click Publish. The gateway worker picks up the published version within a second and starts firing real triggers. Once the flow is running, clicking Save automatically pushes further edits live — you don't need to click Publish again for every change.
Node Library
Triggers
Tag Change — Fires whenever a tag value changes. Configure a tag path and optionally "Changed only" to filter out no-op writes (where the new value equals the old one).
Manual Run — Fires when you click Run Now in the inspector. Useful for testing, one-off jobs, and entry points called from the AI or API.
Alarm Event — Fires on alarm state changes (active / inactive / acknowledged). You can scope it to one specific alarm on one tag, or listen to every alarm configured on a tag.
Schedule — Fires on a cron schedule in UTC. Standard 5-field syntax: minute hour day-of-month month day-of-week. Examples: */5 * * * * every 5 minutes, 0 6 * * * daily at 6am.
Webhook — Exposes a URL that fires the flow when POSTed. The secret is generated automatically and appears in the inspector — copy it into your external system.
Tag Operations
Read Tag — Emits the current value of a tag on out. Use mid-flow to fetch a related value you didn't carry in the payload.
Write Tag — Writes a value to a tag. Value can come straight from msg.payload, or from a JS expression (for example msg.payload.value * 2).
Historian Query — Fetches aggregated historical data (avg / min / max / last / count) over a lookback window. Returns a time-series array on out.
Logic
Expression — Runs a sandboxed JavaScript expression over msg.payload. The return value becomes the new payload. Example: msg.payload.temp > 100 ? "HIGH" : "OK".
Branch — Routes the message to true or false based on a JS condition. Example condition: msg.payload.state === "ALARM".
Switch — Multi-way branch. Add as many cases as you need; each is a JS expression. The first truthy case wins (or all of them, if you turn on Match all). Unmatched messages go to else. Case outputs are named case_0, case_1, … in order.
Threshold — Numeric predicate with optional deadband and dwell time. Modes: above, below, between, outside_range. Dwell (for_ms) requires the condition to hold for N milliseconds before firing — useful for silencing spikes.
Change — Sets a value on msg at a dot-path without writing a full Expression. Example path: payload.alarm_level, value expression: "high". Good for attaching timestamps, labels, or flags.
Join — Collects messages from multiple upstream edges and emits them as a single combined payload. Modes: array, object (keyed by source node id), first, latest (reactive CombineLatest style). Send anything to its reset input to clear buffered state.
Delay — Waits the configured milliseconds, then emits the message unchanged. Useful for escalation timers and staggering downstream actions.
Throttle — Emits the first message in each time window and drops the rest to the dropped output. Prevents flapping tags from hammering downstream actions.
Debounce — The opposite of Throttle: waits for a quiet period (wait_ms) with no new messages, then emits the most recent one. Each arrival resets the clock. Good for "only notify if the alarm stays alarmed for N seconds" or collapsing a burst of updates into a single settled event. Send anything to reset to discard the buffered message.
Subflow — Invokes another published flow inline, passing this node's incoming payload as the seed and emitting the child's last payload on out. The target flow picker stores a flowId, but you can also template the field with {{msg.payload.target}} to route dynamically. Max nesting depth is 8 by default.
Actions
HTTP Request — Makes an outbound HTTP request (GET, POST, PUT, etc.) and emits the response on out. Body and URL can reference {{msg.payload.x}} placeholders. Requests to localhost, private IP ranges, and cloud metadata endpoints are blocked by default as an SSRF safeguard — use this node for external APIs, not intra-cluster calls.
Notify — Sends email and SMS via the alarm notifier. Pick users, groups, or type email addresses directly. Subject and body support {{msg.payload.x}} templating.
AI
AI (LLM) — Sends a templated prompt to a language model and returns the assistant's reply on out. Great for summaries, classifying events, or turning structured data into human-readable prose. model, temperature, and max_tokens are all templatable — one node can route cheap-and-fast vs precise-and-slow based on the incoming payload.
Anomaly (Z-Score) — Rolling z-score on a numeric tag. Flags samples that are N standard deviations off the recent mean. No fixed threshold required — ideal for sensors whose "normal" range drifts with ambient conditions. Choose rolling (fixed window) or ewma (exponential, O(1) memory). out fires for every input with the enriched payload; anomaly fires only when |z| crosses your threshold.
Trend Alarm — Detects slow drift that static thresholds miss. Three methods: linear-slope (best for "bearing temp rising 5°F/hour"), ewma (short vs long moving average — good for regime change), cusum (cumulative sum of deviations — good for slow leaks). Supports a dwell_ms to require the condition holds for a sustained period before alarming.
ML Predict — POSTs a sample or rolling window to an external ML service (AWS Lookout for Equipment, Azure Anomaly Detector, a custom Python/ONNX endpoint) and surfaces the score or label on msg.payload. When the response carries a numeric severity above your threshold, the anomaly handle also fires. SSRF guardrails match HTTP Request — private IPs and localhost are blocked.
Working With Payloads
Every message carries a payload. How you reference it depends on the field.
Two Syntaxes
Raw JavaScript — in nodes that evaluate code: Expression, Branch, Switch, Change (value expression), Write Tag (value expression). Use msg.payload or just payload:
msg.payload.temp > 100
msg.payload.state === "ALARM"
Math.round(msg.payload.value * 1.8 + 32)
Double-brace placeholders — in free-text fields like URL, email subject, prompt, tag path. Wrap the reference in {{ }}:
https://api.example.com/sensors/{{msg.payload.id}}/log
Subject: {{msg.payload.tagPath}} crossed {{msg.payload.setpoint}}
Fields that support placeholders show a small fx badge. Hover the help icon on the field for a reminder.
Fan-in: Using Multiple Inputs
Wire two upstream nodes into the same input on a downstream node and both will fire it — separately, one message at a time. Use msg.source.nodeId and msg.source.handle to tell which upstream just fired you:
// Inside an Expression node that receives edges from two branches
if (msg.source.nodeId === "n_alarm") {
return { severity: "high", ...msg.payload };
}
return { severity: "info", ...msg.payload };
If you want to wait for every input before doing something (batch, aggregate, combine), use the Join node instead. Raw fan-in doesn't wait — each arrival runs the node independently.
Testing And Debugging
Live Edge Payloads
As a flow runs, every edge shows the last payload that passed through. Click any edge to open the Connection inspector in the right panel — it shows:
- Source node → target node (with handle names)
- The full payload as pretty-printed JSON
- Reference paths — every leaf in the payload, rendered as a copyable reference like
msg.payload.items[0].value. Click the copy icon next to any path to grab it for use in another node.
The preview is live — while the edge is selected, new payloads update the inspector in place.
Manual Run
Select any Manual Run trigger and use the Payload editor + Run Now button to fire the flow with a payload you control. The JSON payload persists per-node, so you can keep a library of test cases on different manual triggers.
Preview Status Chip
The top-right of the canvas shows a status chip: Preview live (green) means the debug stream is connected and recent messages are flowing, Connected, waiting (amber) means no recent activity, Preview debug error (red) means the stream can't reach the gateway.
AI Assistant
Open the AI popover in flows mode and describe what you want in plain English. Examples:
- "When pump temp goes above 180 for more than 30 seconds, email ops@example.com"
- "Every morning at 6am, fetch yesterday's total flow and POST it to our Slack webhook"
- "Add a switch after the threshold that routes HIGH, MEDIUM, LOW to different notifications"
The AI can search your tags, add and connect nodes, configure them, and rearrange the canvas — all in a single reviewable change. You see exactly what it proposes before it's committed. If it picks the wrong node or mislabels a case, undo with Cmd+Z and refine the prompt.
The flow assistant runs a streaming agent loop, so working with it feels like a real conversation:
- Multi-turn chat — the popover keeps conversation history. Follow up with "now add a throttle before the Notify" or "change the threshold to 200" without re-describing the flow from scratch.
- Nodes appear live — new nodes pop onto the canvas as the AI creates them, and edges light up as they're connected, instead of waiting for a single big before/after.
- Auto layout pass — after adding nodes, the AI tidies up so new nodes align with your existing layout and don't overlap.
- Stop button — cancel mid-turn; the flow is left in the last stable state.
- Live status messages — shows the tool the AI just ran ("searching node types", "configuring Threshold node") instead of a generic spinner.
- Retry on failure — if a tool call fails the AI gets the error and tries a different approach rather than giving up.
Grounding in real data
The flow AI can also query your historian directly — aggregates (avg / min / max over buckets), raw points, or the last N readings — using natural time ranges like "last 5 minutes" or "yesterday". That means you can ask things like:
- "Look at pump RPM over the last hour — what's a reasonable high-threshold for an alarm?"
- "What was the flow rate during yesterday's morning shift? Set the Threshold node's
for_msso we don't fire on normal brief spikes." - "Show me the last 10 readings for tank level — I want a reasonable debounce window."
The assistant pulls the data, reasons about it, and picks node configurations grounded in what actually happened rather than guesses.
Publishing
Drafts persist automatically as you edit. While a flow is in draft, the editor runs a preview executor — the live edge payloads you see come from that preview, not production.
The Publish popover has two modes:
- Flow is stopped — the popover shows a single Publish button. Clicking it snapshots the draft to the published document and enables the flow, so the first publish ships it live in one click.
- Flow is running — the popover shows a single Unpublish button. Unpublishing stops the live executor; the published document is preserved.
Once a flow is running, you don't need to reopen the Publish popover to ship subsequent edits. Clicking the main Save button on a running flow both saves your draft and auto-publishes the changes live — the same way saving a published page does. This matches the pages editor, so working on a flow feels the same as working on a page.
Tag Change, Schedule, and Webhook triggers only fire on the published document, so a flow must be published at least once to run in production.
Version History
The version history panel (available from the toolbar, same as pages) is where you keep named snapshots of a flow's history. Use it to:
- Snapshot a known-good state before making a risky change
- Roll back a flow to an earlier version if something goes wrong after publishing
- Review how a flow evolved over time
A few details worth knowing:
- Snapshots are on-demand — publishing a flow does not automatically create a version row. Create a version manually when you want to mark a point in history.
- You can delete old versions from the panel to keep the list tidy.
- Restore brings the selected version's document back into the draft. You can review it, edit if needed, and publish when ready.
Tips And Common Patterns
- Always start with a trigger. A flow without a trigger never runs.
- Wire the
errorhandle on any node that touches the outside world (HTTP, Notify, Tag Write) — otherwise failures are silent. A Notify or a Change → Tag Write onerrorturns silent failures into visible ones. - Throttle before Notify. A flapping tag with no throttle will page ops every time it twitches.
Throttle (60000ms)→Notifyprevents that. - Use Threshold's dwell (
for_ms) to silence spikes. "Temp above 180 for 30 seconds" is almost always what you want, not "temp above 180 right now." - Debounce to wait for things to settle.
Debounce (5000ms)waits until 5 seconds of quiet, then emits the latest value. Perfect for "only alert after the condition has been stable for a bit." - Join with
latestmode for "I need the most recent value from every branch before I can act." Send anything to itsresetinput to start over. - Pair an AI detector with Notify. Wire
Anomaly (Z-Score)orTrend Alarm'sanomaly/alarmhandle into aNotify— you get early warning on weird sensor behavior without having to pick a fixed threshold. - Subflows for reuse. If three flows do the same "format alarm → send to Slack" pattern, pull that into a published flow and call it via a Subflow node in each.
- Test with Manual Run before publishing. It's cheap. Catches 90% of wiring mistakes.
Next Steps
- Scripting — when a flow's Expression node isn't enough and you want full binding/page/gateway scripts
- Tags & Data Sources — the tag layer that every flow reads and writes
- Dashboards — pair flows with live dashboards so operators see what your rules are doing