Flows

Bird Flows is the runtime for multi-step, multi-channel messaging workflows. Welcome series, drip campaigns, retention loops, OTP fallback chains — defined in code, run on Bird's infrastructure, observable per step.

Concept

A flow is a trigger plus an ordered set of steps plus persistent state. The trigger fires the run (a webhook, an API call, a schedule, or an event from another flow). Each step is durable — runs survive deploys and restarts, and every step is queryable individually.

Step types

StepWhat it does
sendSend a message on any channel (email, SMS, voice, WhatsApp).
waitPause for a duration or until an absolute date.
branchConditional split based on flow state, recipient data, or an inbound event.
parallelFan out multiple steps at once and join on completion.
api_callCall any HTTPS endpoint and bind the response into flow state.
codeRun a small JavaScript snippet on Bird's runtime — for transforms and computed variables.

Defining a flow

Flows can be defined in TypeScript (typed DSL, recommended) or YAML (for config-only deploys). The two are interchangeable — same step types, same compiler.

// welcome-7d.ts
import { defineFlow } from "@bird/flows";

export default defineFlow({
  name: "welcome-7d",
  trigger: { type: "api" },
  steps: [
    { type: "send", channel: "email", template: "welcome", to: "{{ to }}" },
    { type: "wait", duration: "1d" },
    {
      type: "branch",
      when: "{{ events.email.opened }}",
      then: [{ type: "send", channel: "email", template: "tips" }],
      else: [{ type: "send", channel: "sms", template: "nudge", to: "{{ phone }}" }],
    },
    { type: "wait", duration: "6d" },
    { type: "send", channel: "email", template: "week-recap" },
  ],
});
# welcome-7d.yaml
name: welcome-7d
trigger: { type: api }
steps:
  - { type: send, channel: email, template: welcome, to: "{{ to }}" }
  - { type: wait, duration: 1d }
  - type: branch
    when: "{{ events.email.opened }}"
    then:
      - { type: send, channel: email, template: tips }
    else:
      - { type: send, channel: sms, template: nudge, to: "{{ phone }}" }
  - { type: wait, duration: 6d }
  - { type: send, channel: email, template: week-recap }

Deploying a flow

Deploy a flow file from the CLI. The deploy is versioned and atomic — in-flight runs continue on the version they started on; new runs use the latest.

bird flows deploy welcome-7d.ts

You can also wire deploys to GitHub Actions — the CLI is the same binary your CI uses.

Running a flow

Invoke a deployed flow by name. The response carries a flow_run_* ID you can use to inspect state later.

const { data, error } = await bird.flows.run({
  flow: "welcome-7d",
  to: "delivered@bird.dev",
  variables: {
    name: "Ada",
    phone: "+15005550006",
  },
});

if (error) throw error;
console.log(data.id); // "flow_run_2bX91q..."

Observability

Every run is a span; every step inside the run is a child span. Fetch a run by ID or watch it live in the console.

const { data, error } = await bird.flows.runs.get("flow_run_2bX91q...");
{
  "id": "flow_run_2bX91q...",
  "flow": "welcome-7d",
  "state": "running",
  "started_at": "2026-06-01T17:00:00Z",
  "steps": [
    {
      "name": "send-welcome",
      "type": "send",
      "status": "completed",
      "duration_ms": 412,
      "output": { "id": "email_2bX91q...", "state": "delivered" }
    },
    {
      "name": "wait-1d",
      "type": "wait",
      "status": "running",
      "duration_ms": null,
      "output": null
    }
  ]
}

Quiet hours, frequency caps, channel preferences

The runtime applies these per recipient before any send step actually sends. Quiet hours (per recipient's local time zone), per-channel frequency caps, and channel preferences (the recipient's stated preferred channel) are configured once at the account level and the runtime respects them automatically — you don't write the logic in your flow. A step held by quiet hours is paused and resumed at the next eligible window.

Unsubscribe handling

Flows respect per-channel opt-out automatically. A flow run that reaches a step targeting an opted-out recipient skips that step and surfaces a flow.step.skipped event with reason: "recipient_opted_out". Two-line semantics: opt-outs are per-channel (an unsubscribe from email does not silence SMS), and they're enforced at send time, not at run start — so opt-outs that happen mid-run still take effect.

Testing

Run a flow in dry-run mode to get back the planned step list without actually sending anything. Useful in CI, and useful for "what would this flow do if I invoked it now?" debugging.

const { data, error } = await bird.flows.run({
  flow: "welcome-7d",
  to: "delivered@bird.dev",
  variables: { name: "Ada", phone: "+15005550006" },
  dry_run: true,
});

if (error) throw error;
console.log(data.planned_steps); // [{ name, type, would_send_to, ... }]

Error handling

A step that exhausts its retry policy emits flow.step.failed; the run as a whole emits flow.failed. Failed runs land in the dead-letter queue and are replayable.

const { data, error } = await bird.flows.replay("flow_run_2bX91q...");
if (error) throw error;
console.log(data.id); // new flow_run_* id, same flow and variables

Subscribe to flow.started, flow.step.completed, flow.step.failed, flow.completed, flow.failed, and flow.cancelled from the webhooks doc to drive your own alerting.

Commencez avec un seul canal.
Ajoutez les autres quand vous êtes prêt.

Une clé API de test est disponible immédiatement. L'accès production se débloque dès que vous ajoutez un moyen de paiement et vérifiez un expéditeur.

CommencerLire la docou

Vous utilisez Claude Code, Cursor ou Codex ? Connectez-le à notre serveur MCP — 141 outils, un par endpoint API, avec des clés d'agent à portée limitée.