# Flow Builder

> The visual flow editor — one route, a canvas over strongly-typed plugin cards, and why every mutation result is the only state there is.

Page: https://sdk.chatfuel.com/docs/modules/flow-builder
Markdown: https://sdk.chatfuel.com/docs/modules/flow-builder.md

## What it is [#what-it-is]

There is no JSON anywhere in a flow. Every plugin card is its own GraphQL type with its own
family of single-field setter mutations — about **230** of them — so editing a flow means
calling one setter per field and branching on `__typename` for everything you draw. And there
are no flow-builder subscriptions and no revision field: each mutation answers with the block or
the flow it changed, and that answer is the only state there is.

The module is one screen: the flow picker on the left, the canvas in the middle, the block
inspector on the right, and a Test dock floating over the canvas. It is **not** the AI's
behaviour — that is a different surface with a different model.

## Installed as [#installed-as]

|             |                                               |
| ----------- | --------------------------------------------- |
| Wizard id   | `flow-builder`                                |
| Install it  | `npx @chatfuel/wizard --modules flow-builder` |
| Selected by | Default — `--yes` installs it                 |
| Requires    | Nothing, beyond the implicit `core`           |
| Recommends  | `contacts`                                    |
| Skill       | `chatfuel-flow-builder`                       |

Neither `hidden` nor `railHidden`: an ordinary rail item and an ordinary `/flow-builder` route.

## Routes and views [#routes-and-views]

One path and no view segment — the canvas is the whole surface, and which flow is on it lives
in the query string.

| Path            | What it shows                                                                                |
| --------------- | -------------------------------------------------------------------------------------------- |
| `/flow-builder` | The flow picker, the canvas for the selected flow, the inspector, and the Test dock over it. |

| Parameter | What it does                                                                                                                         |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `flow`    | The flow on the canvas. Absent means nothing is open yet.                                                                            |
| `b`       | The block to select on load. Read once at mount and dropped when you switch flows — it belonged to the flow the link was minted for. |

Both are rewritten from the current selection, so the address in the bar is always a link
somebody else can open. A new flow is created for one of five platforms — WhatsApp, Instagram,
Facebook, TikTok or the widget — and the block palette narrows to that platform's families plus
the platform-less ones.

The Test dock is open by default and collapses to a pill with `T` or from the ⌘K palette. Below
the narrow breakpoint it stops being a floating window and becomes a bottom drawer: at 600px the
canvas has no rectangle to spare.

## The model [#the-model]

Four levels, and only the middle two are things you place:

```
Flow ─ blocks[]        (16 concrete Block types, x/y canvas coords)
     ─ connections[]   (derived edges)
     ─ startingPointBlock
     ─ entryPoints[]

Block ─ blockElements[]   (the "plugin" cards, 29 concrete BlockElement types)
Element ─ buttons/rows/handles (ComponentHandleID — connection sources)
```

**There is no `createBlock`.** A block is born through its plugin: `<plugin>CreateWithBlock`,
`<plugin>CreateWithBlockAndConnection` (block *and* the edge that points at it, in one atomic
call — this is what dragging an edge into empty canvas does), or `<plugin>CreateInBlock` for the
families that stack. `createFlow` takes no name at all; the server names it and
`updateFlowName` renames it. There is no clone, duplicate, import or export.

**Render all sixteen block types and all twenty-nine element types, or the canvas crashes** the
first time the product ships a new one. Real flows can also carry types that are not in the
bundled schema at all — internal integrations — and the interface-level fields still come back
for those, which is what the generic fallback is for. This module's coverage is a test rather
than a promise: twenty-four dedicated editors plus five that are read-only because the schema
has no setters for them, and a new typename with no decision recorded fails the suite.

**A new content element carries `waitForReplies: true`.** At runtime the flow stops at that
block until the contact answers, rather than running on to the next one. Set it to `false` for
an uninterrupted chain.

<Callout type="warn">
  Mutations **succeed while leaving the element invalid**. Validation is not an error path here:
  it is a field, `BlockElement.errors`, recomputed by every mutation, and a save that produced
  eleven validation errors returns a perfectly ordinary success. Re-read `errors` from every
  mutation result, not only from the initial query. The one place it turns into a refusal is
  `blockEnableEntryPoint`, which answers `ComponentHasValidationErrors` — by which point the
  flow has been broken for as long as nobody looked.
</Callout>

Delete is the one operation with no inverse. Creating a block again gives it a **new id** and an
empty copy: every setter's contents are gone, and every connection that pointed at it stays
broken. That is why undo here is a compensating *forward* mutation rather than a rollback, and
why a ⌘Z that cannot undo says so instead of doing nothing.

Because there are no subscriptions, this module can do something the live surfaces cannot: paint
the canvas from a local snapshot of the last `FlowStructure` before the network answers.

**The Test dock is a real conversation, not a simulation.** `previewResponsesStartInFlow` mints
a preview conversation on a synthetic contact and runs the flow from its starting point; sends
are injected as synthetic provider webhooks and travel the production pipeline, so WhatsApp
behaves like WhatsApp. Everything downstream is the ordinary Inbox API — the same
`messageAdded`, the same merge-by-`clientId`. Flows are the only preview target with a readback
(`Flow.previewResponsesSession`), which is why the dock survives a reload; an automation's test
session lives in memory and starts over.

## Configuration [#configuration]

| Variable                     | Notes                                           |
| ---------------------------- | ----------------------------------------------- |
| `CHATFUEL_TOKEN`             | Secret. Server-side only.                       |
| `VITE_CHATFUEL_WORKSPACE_ID` | The workspace you picked during the wizard run. |
| `CHATFUEL_API_BASE`          | Defaults to `https://panel.chatfuel.com`.       |

Every variable the app reads, and which side reads it, is on
[environment variables](/docs/deploy/environments).

## Permissions [#permissions]

| Object  | Action | Required for                                                   |
| ------- | ------ | -------------------------------------------------------------- |
| `Flows` | `View` | Rendering the canvas — and starting a test.                    |
| `Flows` | `Edit` | Content editing: every setter, every create, every connection. |
| `Inbox` | `View` | Reading the Test dock's preview conversation.                  |
| `Inbox` | `Edit` | Sending messages and pressing buttons in it.                   |

The split is deliberate and visible: a token with only Flows permissions can start a test and
read the thread, and the composer says why it is closed.

## Limits [#limits]

**No history, no revision, no revert — last write wins.** Two people on one flow will not
collide with an error; the second write simply stands. Poll `bot.flow(flowID)` if you need
cross-client freshness.

**The flow list has no pagination.** `flowGroups`, `flowsWithoutGroup` and `defaultReplyFlows`
come back as three flat arrays, whole, every time.

**Four plugin kinds do not exist in this API**: gallery, carousel, quick replies and delay. And
`facebook` has no flow-builder blocks at all — a Facebook flow appears in the picker, opens on
an empty canvas, and the dashboard refuses a Facebook preview session.

**Two reads are traps.** Selecting `Block` *interface* casts together with a wide
`blockElements` selection makes the federation router hang for about sixty seconds and answer
`InternalServerError`; use concrete-type casts, which plan instantly for the same data. And
`ContentBlock.stats` returns `InternalServerError` per block in production — it is nullable, so
asking for it poisons an otherwise good response.

**A test is pinned to one flow, and it leaves no trace.** No message carries a component or
block id and `MessageSender` has no component variant, so there is no "which block said this"
and no highlight-the-current-block. There is no stop or reset mutation either: restart calls
`FlowTestStart` again and the panel hides the older rows behind a client-side watermark.
Attachments, voice sends, template sends and starting from a chosen block are all absent from
preview. A flow with no starting point cannot be run at all.

The module's own ceilings:

|                                 |                                 |
| ------------------------------- | ------------------------------- |
| Undo history                    | 50 steps                        |
| Local first-paint snapshot      | 1,000,000 characters            |
| Flow prefetch cache             | 8 flows, 30 seconds             |
| Auto-layout spacing             | 320px per column, 220px per row |
| Attribute suggestions per page  | 100                             |
| Upload: images                  | 4 MB                            |
| Upload: video, audio, documents | 15 MB                           |

Uploaded media is temporary until it is attached to an element — unattached files are collected.

<Cards>
  <Card title="Flow Builder, as a product" href="/modules/flow-builder" description="The twenty-six block families, the editors, the keyboard and the test dock, from the builder's side." />

  <Card title="AI Agent" href="/docs/modules/ai-agent" description="The other automation surface, and not this one: how the AI behaves when a message arrives, rather than scripted canvas logic." />

  <Card title="Inbox" href="/docs/modules/inbox" description="The API behind the Test dock — a preview conversation is an ordinary conversation." />

  <Card title="Your agent's notes" href="/docs/concepts/agent-skills" description="The wizard installs .claude/skills/chatfuel-flow-builder/ (Codex: .agents/skills/chatfuel-flow-builder/) — the plugin catalog, the creation triad, TemplateStr, validation-as-state, and references/test-panel.md for the dock." />
</Cards>
