Skip to main content

Flow Builder

The Orbit Flow Builder is a visual drag-and-drop editor for designing communication workflows. Drag nodes onto the canvas, connect them, and deploy — the same definition powers both the visual editor and the flow API.

Accessing the Builder

Open the Flow Builder from the Orbit dashboard: Flows > Create New Flow, or open an existing flow. The builder is backed by every flow’s saved definition, so anything you build here is also readable and writable through the flows API.

The Canvas

  • Left sidebar — the node palette. Drag a node onto the canvas to add it.
  • Canvas — connect nodes by dragging from a node’s output handle to the next node’s input handle.
  • Right sidebar — opens when you select a node. This is the config panel: every field the node accepts at runtime.
  • Toolbar — Save, Deploy, Take offline, Test, Forecast, Versions, Export, Load, undo/redo, auto-layout, and validate.

Node Types & Config Panel Fields

Click a node on the canvas to open its config panel. Every field below is what the runtime reads when the flow executes — nothing is decorative. All nodes also expose two shared fields at the top of the panel:
  • Label — the display name on the canvas.
  • Output Variable — names the variable this node writes its result into. Downstream nodes reference it as {{flow.variables.<name>}}. The panel also lists the flow’s currently-known variables as copyable chips.

Triggers

Every flow needs at least one trigger node. Set the Trigger Type first — the rest of the panel adapts to it. All trigger types also accept an Activation Condition — an optional filter (e.g. body contains START) that gates whether the event actually starts the flow. Blank means every event fires. For Comment Received this field becomes the Keyword Filter and adds a Private reply (DM) message field, with {{author}}, {{keyword}}, and {{comment}} available for personalisation.

Actions

Text fields accept {{variable}} placeholders. A send node with an empty to or body records a skipped step instead of sending.

AI

Logic

A/B Test

The A/B Test node routes each contact down exactly one weighted variant. The panel is a variants editor:
  • Variant name + Weight % per row; at least two variants are required.
  • Leave every weight at 0 for an even split, or set weights that sum to 100% (the panel shows the running total and flags a sum that drifts from 100%).
  • Wire each variant’s output handle to the step it should run. A contact stays on its assigned variant for the life of the flow.

Functions (Transform)

The Functions node derives values into the flow without a server. Each assignment row writes one output variable. Up to 50 assignments per node.
  • Output variable — the flat target key written into the flow context (letters/digits/underscore; must not start with a digit or _).
  • Source — one of:
    • Template — a {{var}} template string, e.g. Hi {{first_name}} from {{city}}.
    • Expression — a safe boolean expression using the same operator set as the Condition node, e.g. score >= 50.
    • Literal value — written as-is.
Downstream nodes read the result with {{flow.variables.<name>}}. Invalid assignments are skipped (recorded in the step trace); the flow keeps routing.

Condition Operators

Condition and Functions expressions share one safe, bounded evaluator — there is no eval. You can use:
  • Comparison: ===, !==, >, <, >=, <=
  • Logic: &&, ||, !, parentheses
  • String methods: .includes(...), .startsWith(...), .endsWith(...)
  • Literals: strings ('x' / "x"), numbers, true / false, null
  • Dot paths against context values, e.g. contact.tags.includes('vip')
Identifiers such as require, process, eval, window, and similar are rejected, and expressions over 10,000 characters fail. A numeric comparison against a non-numeric value evaluates to false.

Variables

Reference dynamic data in any text field with double curly braces. Variables are flat keys{{first_name}}, not {{contact.first_name}}. Three sources feed a flow’s context:
  1. Trigger payload — the inbound event’s fields (from, body, message, and any keys the trigger supplies), plus contact context.
  2. Node outputs — each node’s Output Variable writes {{flow.variables.<name>}} for downstream nodes.
  3. Functions assignments — derived keys from a transform node.
Rules:
  • Flat keys only. Template placeholders and transform targets are top-level identifiers — a transform rejects dot-paths and a key starting with _ (that namespace is reserved for the executor’s internal bookkeeping).
  • Missing keys render empty. A placeholder whose key is absent, null, or undefined renders as an empty string (and is logged) rather than failing the send.
  • Email bodies are HTML-escaped. Interpolated values in Send Email are escaped so a webhook-supplied payload can’t inject markup into the recipient’s mail client.
  • PII is redacted on outbound webhooks. Phone numbers, emails, message bodies, and similar keys are stripped to [REDACTED] in the context sent to an HTTP Request node’s external URL — use flow variables for fields you intend to expose.

Limits

Structural validation (run on publish and on demand) rejects a flow with no nodes, no trigger node, disconnected nodes, or a misweighted A/B Test.

Testing a Flow

Open Test in the toolbar. The panel does two things:
  • Validate Flow — runs structural validation on the saved flow and lists human-readable errors (disconnected nodes, missing trigger, misweighted A/B variant). Deploy is blocked until validation succeeds. Save the flow first — validation needs a persisted flow.
  • Run test — executes the flow against a sample payload:
    • Test Recipient — the phone number merged into the trigger payload as from.
    • Trigger Data (JSON) — the free-form object the trigger receives, e.g. { "from": "+14155550100", "body": "Hello" }. Invalid JSON and non-object input are rejected inline before the run starts.
As the test runs, the builder highlights each node on the canvas as it executes so you can trace the exact path taken — including which Condition branch, which A/B variant, and any step recorded as skipped or errored. The result banner reports the run status and, on failure, the reason. The Forecast button beside Test estimates reach, spend, and timing from the current graph without sending anything — a pre-deploy what-if check.

Publish, Unpublish, and Status

  • Save writes a draft. Drafts never execute live.
  • Deploy publishes: it validates, then locks the current definition as the live version and starts processing matching events immediately. flow.published fires for your subscribed webhooks.
  • Editing a published flow creates a draft version alongside it — the live definition keeps running until you save and Deploy again.
  • Take offline (unpublish) flips status back to draft: the flow stops accepting new executions but keeps its design, version history, and execution records. Nothing is lost — re-Deploy to resume. Only publish/unpublish transitions are allowed; you can’t publish an archived flow.
  • Delete (in the Load menu) permanently removes a saved flow and is confirmed separately from deleting canvas nodes.

Version History & Rollback

Every save and every publish writes an immutable version snapshot (publish snapshots the definition and the status flip in one transaction). Open Versions in the toolbar. The panel lists every version with its number, author, timestamp, node/edge counts, and a diff summary. For each entry:
  • Preview — loads that version’s graph onto the canvas read-only so you can inspect exactly what changed.
  • Restore — replaces the current draft with that version’s graph (after a confirm dialog; unsaved changes are lost). Restoring doesn’t change status by itself: restoring onto a published flow keeps it published until you Deploy the restored draft.

Edge Cases & Failure Behaviour

  • Error edges — a node that throws records output = { error } and routes to its error edge (or its default edge when none is wired). Validation helpers like AI Response, Update Record, and Send Survey return failures as data ({ error: "..." } / { skipped: true, reason }) so the flow keeps routing to a fallback handler instead of aborting the run.
  • Condition with no match — a Condition evaluates to a boolean; wire the false side to the path you want when no comparison holds. A numeric comparison against non-numeric data is false, not an error.
  • AI Classify routing — the classifier’s detected intent matches an edge by handle/label, and falls back to the yes edge, then the no edge, then the default edge. If the message or intents list is empty the step is recorded as skipped.
  • A/B determinism — assignment is a pure hash of contact + node bucketed against cumulative weights. A resume or re-entry re-derives the same variant with no stored state, so one contact always sees one variant. Enabling adaptive bandit_mode on a node routes traffic toward the live-leading variant using the flow’s own win-rate rollup, and falls back to the static split when no live data exists.
  • Send fallback — a send node may carry a fallback (channel / to / body) used when the primary send can’t complete.
  • Emergency numbers — a Make Call node to an emergency short code (911 / 112 / 999 / 000) is rejected: Orbit has no E911 wiring on flow-initiated calls.
  • Loops — the 500-step guard terminates runaway cycles and records it on the run.

Nodes for API-Defined Flows

Flows created through the API can use node types the visual palette does not surface. Add them to the definition’s nodes array; the runtime executes them identically: Additional send channels — Viber, LINE, Telegram, Instagram, Messenger, push, and fax — are available as send nodes in API-defined flows.

Post a definition

The full round-trip — POST a definition, GET it back, PUT an update — uses one shape: definition is a nodes + edges graph with each node carrying a camelCase type and its config under data. This example wires an inbound webhook into a Condition that replies by SMS only when the message contains SUPPORT. POST /api/v1/flows — create the flow (it is saved as a draft at version 1):
201
GET /api/v1/flows/flw_8f3c9a2b — the response body is data.definition, ready to edit locally:
200
PUT /api/v1/flows/flw_8f3c9a2b — partial update. Send back the whole definition with your edits (here: a wider keyword match) and the flow stores it as a new version:
200
Three shape rules the examples rely on:
  • Branch handles on a Condition — the executor routes the true side down the edge with sourceHandle: "yes" and the false side down sourceHandle: "no". Unlabeled edges follow the fallback (label Yes/No), but API-authored definitions should set sourceHandle explicitly.
  • Trigger source and node kind — the top-level trigger_type is the coarse transport (webhook, schedule, event, manual, workflow); the fine-grained trigger kind (Incoming SMS, Web, a Business Event name) lives in the trigger node’s data.triggerType, matching what the builder stores. The validation endpoint checks for a trigger node and rules out unconditional delay-free loops before you publish.
  • Full replacement on PUTdefinition replaces the graph you sent before; it is not merged node-by-node. Because every PUT on a published flow increments version, GET-then-PUT is the safe pattern for flow-as-code.

Mixing Builder and API on One Flow

The builder and the API edit the same saved definition, so you can:
  1. Build the visual skeleton in the builder and save it.
  2. GET the flow’s definition, append API-only nodes (an Update Record, a Send Survey, an extra channel) and wire their edges, then PUT it back.
  3. Reopen the flow in the builder — API-only nodes render on the canvas (they’re not draggable from the palette, but existing instances display and route normally) and the visual nodes remain fully editable.
Keep weights, trigger sources, and variable keys builder-editable: save-time validation and publish validation run against the merged definition either way. For a full move, Export downloads a portable orbit.flow.v1 JSON of the flow.
Not yet available in the visual palette: Wait for Reply and Loop (per-item iteration). For a multi-way branch, chain Condition nodes (each is an if/else fork).

Keyboard Shortcuts