Skip to content
ChatfuelSDK
Modules

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 idflow-builder
Install itnpx @chatfuel/wizard --modules flow-builder
Selected byDefault — --yes installs it
RequiresNothing, beyond the implicit core
Recommendscontacts
Skillchatfuel-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.

PathWhat it shows
/flow-builderThe flow picker, the canvas for the selected flow, the inspector, and the Test dock over it.
ParameterWhat it does
flowThe flow on the canvas. Absent means nothing is open yet.
bThe 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

VariableNotes
CHATFUEL_TOKENSecret. Server-side only.
VITE_CHATFUEL_WORKSPACE_IDThe workspace you picked during the wizard run.
CHATFUEL_API_BASEDefaults to https://panel.chatfuel.com.

Every variable the app reads, and which side reads it, is on environment variables.

Permissions

ObjectActionRequired for
FlowsViewRendering the canvas — and starting a test.
FlowsEditContent editing: every setter, every create, every connection.
InboxViewReading the Test dock's preview conversation.
InboxEditSending 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 history50 steps
Local first-paint snapshot1,000,000 characters
Flow prefetch cache8 flows, 30 seconds
Auto-layout spacing320px per column, 220px per row
Attribute suggestions per page100
Upload: images4 MB
Upload: video, audio, documents15 MB

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

On this page