> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Build your first automation flow

> Design, test, and debug an Orbit Flow end to end — split a lead-welcome automation into triggers, branching, AI routing, and variables, then catch a failed run with the Executions API before it slips in production.

# 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](/flows/overview) covers the concepts and the full node-type table, and the [Flow Builder](/flows/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.

<Frame>
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/zeelaltd/images/flows/first-flow-canvas.png" alt="The finished lead-welcome flow on the builder canvas" />
</Frame>

## 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:

| Trigger            | Starts when                                                            |
| ------------------ | ---------------------------------------------------------------------- |
| **Business Event** | A platform event fires — here, `contact.created` for every new contact |
| **Webhook**        | An external system posts to the flow's webhook URL                     |
| **Schedule**       | A cron expression fires, for batch jobs                                |

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:

```json theme={null}
{
  "trigger_type": "event",
  "definition": {
    "trigger": { "type": "event", "event": "contact.created" },
    "nodes": [ ... ],
    "edges": [ ... ]
  }
}
```

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](/flows/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:

```
Source is signup_form? ── yes ──> Send SMS ──> Delay 24h ──> (AI routing)
                     └─ no ──> End
```

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:

```
AI Classify
├── interested ──> Run Agent (sales agent)
├── question  ──> Run Agent (support agent)
├── complaint ──> Webhook (alert the team)
└── other     ──> End (leave for manual follow-up)
```

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:

```json theme={null}
{
  "id": "derive_vars",
  "type": "transform",
  "data": {
    "assignments": [
      { "target": "lead_segment", "expression": "order_total > 100" },
      { "target": "greeting", "template": "Hi {{first_name}}" },
      { "target": "followup_hours", "value": 24 }
    ]
  }
}
```

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:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/flows/flow_abc123/simulate \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"audience_size": 50000, "default_country": "US"}'
```

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:

```bash theme={null}
curl -G https://api.orbit.devotel.io/api/v1/flows/executions \
  -H "X-API-Key: dv_live_sk_..." \
  --data-urlencode "flow_id=flow_abc123" \
  --data-urlencode "status=failed"
```

Then fetch one run's full step trace:

```bash theme={null}
curl https://api.orbit.devotel.io/api/v1/flows/executions/exec_xyz789 \
  -H "X-API-Key: dv_live_sk_..."
```

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](/flows/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](/webhooks/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):

```json theme={null}
{
  "id": "followup_ab",
  "type": "abTest",
  "data": {
    "variants": [
      { "id": "sms",     "weight_pct": 50 },
      { "id": "email",   "weight_pct": 50 }
    ]
  }
}
```

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](/api-reference/endpoints/flows) with everything wired in one JSON definition — nodes, edges, and handles match what the builder would draw:

```json theme={null}
{
  "name": "Lead welcome — sentiment routing",
  "trigger_type": "event",
  "definition": {
    "trigger": { "type": "event", "event": "contact.created" },
    "nodes": [
      {
        "id": "gate_source",
        "type": "condition",
        "data": { "field": "source", "operator": "equals", "value": "signup_form" }
      },
      {
        "id": "welcome_sms",
        "type": "sendSms",
        "data": {
          "to": "{{phone}}",
          "body": "Welcome to Acme, {{first_name}}! We'll text you a getting-started tip tomorrow."
        }
      },
      { "id": "wait_24h", "type": "delay", "data": { "amount": 24, "unit": "hours" } },
      {
        "id": "classify_reply",
        "type": "aiClassify",
        "data": { "classify_intents": "interested,question,complaint,other" }
      },
      {
        "id": "sales_agent",
        "type": "aiAgent",
        "data": { "agent_id": "agent_sales_01" }
      },
      {
        "id": "support_agent",
        "type": "aiAgent",
        "data": { "agent_id": "agent_support_01" }
      },
      {
        "id": "alert_team",
        "type": "webhook",
        "data": {
          "url": "https://example.com/hooks/lead-escalation",
          "method": "POST"
        }
      }
    ],
    "edges": [
      { "source": "gate_source", "sourceHandle": "yes", "target": "welcome_sms" },
      { "source": "welcome_sms", "target": "wait_24h" },
      { "source": "wait_24h", "target": "classify_reply" },
      { "source": "classify_reply", "sourceHandle": "interested", "target": "sales_agent" },
      { "source": "classify_reply", "sourceHandle": "question", "target": "support_agent" },
      { "source": "classify_reply", "sourceHandle": "complaint", "target": "alert_team" },
      { "source": "alert_team", "sourceHandle": "error", "target": "notify_ops" },
      { "source": "alert_team", "target": "end" }
    ]
  }
}
```

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

* [Flows overview](/flows/overview) — trigger and node-type reference
* [Flow Builder](/flows/builder) — the canvas and every node
* [Flow Executions](/flows/executions) — the full Executions API field reference
