Skip to main content

Build your first automation flow

By the end of this guide you will have shipped a real multi-channel flow — one that welcomes a new lead, branches on the sentiment of their reply, and routes the conversation to an AI agent or a human — and you will know how to debug it with the Executions API when a run misbehaves. This is a design walkthrough, not a node dictionary. The Flows overview covers the concepts and the full node-type table, and the Flow Builder reference lists every node, shortcut, and visual option. For each step below, the guide shows both the canvas path and the API-defined JSON.
The finished lead-welcome flow on the builder canvas

1. The use case — welcome a new lead, route by sentiment

Sketch the narrative before touching the builder. You want to:
  1. Start when a contact is created (web form, API, import).
  2. Send a welcome SMS.
  3. Wait 24 hours, then send a follow-up.
  4. Classify the contact’s reply with AI: interested, question, complaint, or other.
  5. Route interested and question replies to your AI agent; alert your team in Slack on a complaint; leave other for manual follow-up.
One loop exists here — turn one inbound lead into a handled conversation — and it decomposes into a single linear path with one AI branch. Decomposition first, canvas second, is what keeps flows readable.

2. Choose a trigger and shape the payload

Every flow starts with exactly one trigger. The common ones: For the welcome flow, a Business Event trigger on contact.created is the simplest choice:
  • In the builder: drag a trigger onto the canvas, set the trigger type to Business Event, and pick contact.created.
  • In the API definition: trigger_type is event and definition.trigger carries the detail:
The trigger payload lands in the flow context as flat keys. The event object above hands you phone, email, first_name, last_name, and the rest of the contact record; a webhook trigger hands you the POSTed JSON body flattened the same way. Flat naming matters — see section 5 before you paste {{contact.first_name}} into a node. Gate noisy triggers. contact.created fires on imports and API batches too, so for this design a Condition node checks source equals signup_form before anything is sent. A trigger filter you add after a flow has quietly SMS’d 50,000 imported contacts is a bad day.

3. Lay out nodes — messages, delays, multi-way branching

Connect nodes in the order a visitor moves through them. For this flow:
  1. Condition — source gate. Five comparison operators are available: equals, not_equals, contains, greater_than, less_than. Here, field = source, operator = equals, value = signup_form. Connect the yes edge onward; the no edge ends the run.
  2. Send SMS — the welcome. One messaging node per channel, to and body in the node’s settings. body = Welcome to Acme, {{first_name}}! We'll text you a getting-started tip tomorrow. See Flow Builder for WhatsApp, email, RCS, and the other channels.
  3. Delay — the cadence. Amount 24, unit hours. Long waits (over a minute) park the run — it shows waiting on the Executions API and resumes automatically at its scheduled time.
For branching, each Condition is an if/else fork; multi-way logic is a chain of them:
Do not reach for a Loop node. loop is not a recognized node type at runtime — an unrecognized type is skipped, and a skipped iterator on a contact list is the classic spray-and-pray bug. Batch work belongs to a Schedule-triggered flow or Campaign enrollment; per-contact iteration can wait on the roadmap.

4. AI routing — classify, branch, hand to the agent

The AI nodes turn the flow into a router, not a fixed sequence:
  1. AI Classify. Set the candidate intents as a comma-separated list, e.g. interested,question,complaint,other. The node classifies context.message / context.body, and it routes on the classified intent: an edge whose sourceHandle (or label) matches the intent name wins, with yes and no handles as fallbacks.
  2. Run Agent. data.agent_id (or agentId) points at an AI agent you have provisioned. The contact’s message goes to that agent, and the flow continues down the default edge once the agent returns.
  3. Webhook — human alert. The complaint branch POSTs the run context to your team’s endpoint — the executor sends a body with execution_id and the sanitized context. Public URLs only: the node validates the URL and rejects private/internal addresses, and only the approved HTTP methods pass.
The routing design:
Draw an explicit error edge on any node that can fail — the executor routes failures down an edge marked error if you draw one, and falls back to the default next node only when you did not. An HTTP Request to a flaky endpoint without an error edge is a run that finishes on the default path with a hidden failure; route it on error to a fallback (often an email to ops) or end the run cleanly.

5. Variables and templating

Variables are flat keys. In every text field, {{first_name}} resolves against the merged context of the trigger payload, the contact record, and values earlier nodes wrote — a missing key renders as an empty string. Dotted paths like {{contact.first_name}} never resolve; write flat placeholders. In the builder you capture values from an upstream node’s output (for example an HTTP Request response) and reference them downstream with {{...}}. In API-defined flows a transform (Functions) node derives values explicitly, using an assignments array — string template, a safe boolean expression, or a literal value — written into top-level keys:
Target names are plain top-level identifiers — no leading underscore, no dots. The node safe-evaluates the expression against the context and writes only keys that pass that gate.

6. Simulate before you publish

Two simulators cover two different questions: Test mode (canvas). Click Test in the builder toolbar to walk a flow with sample data. The builder highlights each node as it executes, so you can watch the exact branch the run takes before a real contact goes through it. Journey simulator (API). Before a launch, dry-run the flow against a real audience — no contact is enrolled, nothing is sent:
The response projects the per-step drop-off funnel, predicted conversions, the channel mix, and a projected send cost priced at your live rates. For campaign journeys the same pre-activation preview sits at POST /campaigns/journeys/simulate. Run it before every launch; a 50,000-contact typo discovered here is cheap, in production it is not.

7. Publish, then watch the first runs

Publish the flow — the API handles are POST /flows/:id/publish and POST /flows/:id/unpublish. Once live, debug with the Executions API. List failed runs for your flow:
Then fetch one run’s full step trace:
The trace carries each step’s status, rendered input and output, and the error message, keyed by node_id and node_type so you can match it to the definition. The full field reference, status table, and error codes live in Flow Executions. Work through a failed run the same way every time:
  1. Read error on the list row — the executor surfaces the last failed node and its reason.
  2. Fetch the run and walk the steps array in execution order.
  3. At the failed step, expand the rendered input. A placeholder like {{phone}} that resolves to a null to field, or a complaint edge that points at no target, shows up here.
  4. Compare steps_completed to total_steps to see where the run stopped.
  5. Subscribe to flow.execution.failed over webhook events so failures page you instead of hiding in the list.

8. Iterate — version history, A/B tests, dashboards

Version history. Every save creates a server-side snapshot. GET /flows/:id/versions lists them, POST /flows/:id/versions takes a manual snapshot, and POST /flows/:id/versions/:versionId/restore rolls back onto the draft. Restoring on a published flow overwrites the draft, never live traffic — the live publish stays active until you re-publish the restored draft. In the builder, compare and roll back from Flow Settings > Version History. A/B test nodes. An abTest node routes contacts down weighted variants and reports a per-variant winner. Validation requires at least two variants, and explicit weight_pct values must sum to 100 (an all-absent weight set splits evenly):
Each variant routes through a static edge handle (handleId) or an explicit downstream path_node_id; an optional bandit mode adapts weights from the flow’s live per-variant results instead of the static split. Analytics. The Flows dashboard consumes the same funnel the simulator blends in, so the assumed drop-off converges on real history as runs accumulate.

Appendix — the worked example as an API definition

Create the flow through the Flows API with everything wired in one JSON definition — nodes, edges, and handles match what the builder would draw:
The no branch of the gate and the other intent simply stop — a condition or classify edge with no target ends the run cleanly. The notify_ops and end ids in the error-handling edges are placeholders for whatever you hang off the error path; the executor routes a failed node down the edge marked error before falling back to the default next node.

Recap checklist

  1. Design the narrative first; decompose into a linear path with one branch where possible.
  2. Pick one trigger and gate it — especially contact.created, which fires on imports.
  3. Chain Condition forks for multi-way logic; do not expect a Loop node (loop is unrecognized and skipped).
  4. Intent-edge routing on AI Classify; error edge on every fallible node.
  5. Flat {{...}} keys everywhere; derive values with a Functions (transform) node in API-defined flows.
  6. Simulate with Test mode and POST /flows/:id/simulate before you go live.
  7. Publish, watch the Executions API, debug by walking the step trace.
  8. Iterate with version snapshots and A/B nodes.

Next steps