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.
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
| 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
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
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.
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.
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
| 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.
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
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.
Flow Builder, as a product
The twenty-six block families, the editors, the keyboard and the test dock, from the builder's side.
AI Agent
The other automation surface, and not this one: how the AI behaves when a message arrives, rather than scripted canvas logic.
Inbox
The API behind the Test dock — a preview conversation is an ordinary conversation.
Your agent's notes
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.
AI Agent
How the bot's AI behaves on each of eighteen entry points — the module's one route, the inheritance model behind it, and where the API stops.
Knowledge Base
Everything the AI knows about a business, as one structured record — the eight sources, the character budget, and why there is nothing to upload.