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, with meta carrying total, page and page_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.