How a bot works
Read this once and the Bots API reference will make sense.
A bot has two sides
The draft is what you are editing. Published versions are what your visitors talk to.
They are separate. Writing the draft changes nothing for a visitor. Publishing copies the draft over and files the result as a version, and the history of versions is append-only.
edit the draft ──▶ PUT /bots/{id}/test ──▶ try it yourself
└─▶ POST /bots/{id}/versions ──▶ visitors get it
Publishing is creating a version. There is no separate publish call.
A diagram is yours
The draft holds a diagram: blocks and the connections between them. It travels through the API
all but unchanged — keys we do not model, like notes and embedded bricks, come back as you wrote
them. The one thing we add is isTarget: true, on any node that does not already say whether a
connection may end at it.
{
"nodes": {
"hidden": {"id": "hidden", "path": "hidden", "template": "hidden", "params": {}},
"welcome": {"id": "welcome", "path": "welcome", "template": "var_text",
"params": {"version": 3, "text": "Hi! What is your name?", "destination": "name"}}
},
"connections": {
"hidden.success--welcome": {"id": "hidden.success--welcome", "type": "success",
"sourcePath": "hidden", "targetPath": "welcome"}
}
}
A node names its block by template, the storage name. The semantic name GET /blocks lists —
ask_question for the one above — is what the block-level operations address a block by.
Two ids matter. hidden is the start point, and welcome is the greeting slot — the first thing a
visitor is answered with. On every channel family but landbot the greeting slot is called
bot_start.
A connection's type is the output it leaves from. Outputs are not uniform: some carry a $ prefix
and some do not. GET /blocks tells you which, per block.
Saving never loses work
A write to the draft stores what you sent even when it breaks the rules. You get back save_state
and a list of violations, and you can keep editing.
The rules are only enforced when the bot has to run: PUT /bots/{id}/test and
POST /bots/{id}/versions both refuse while any violation stands.
A handful of problems are refused on the spot instead, because there would be nothing sensible to store — a block of a type we do not know, an id already taken. When a call fails lists which.
What answers look like
- One thing comes back under
data. - A list comes back under
data, withmetacarryingtotal,pageandpage_size. -
A field with no value is left out rather than sent as
null. Two answers are the exception and keep their nulls: the draft, and a single version — both carry a stored diagram, and dropping empty values would reach inside it.
Identifiers: a bot, a version and a channel are uuids. A workspace and a person are integers. A block is identified by its id inside the diagram, which is any string the diagram gave it.
Timestamps are UTC, written 2026-09-16T09:18:19. No offset and no fractions of a second — if
you parse them as local time you will be out by your own offset.
Bots built by an older builder
Some bots were made by a builder that came before this API. You can read them, and you can move them
in and out of the trash. Every other write answers 422. They have to be migrated first.
What this API does not do
It does not run conversations. There is no endpoint that accepts or returns what a visitor sees mid chat. You describe a bot here; the runtime is elsewhere.
Where a bot is reached — its web page, its design, its CSS — belongs to the Channels API.