Skip to content
ChatfuelSDK
Modules

Copilot

The operator's own assistant, on a page of its own — what it can read, what it may move, and the approval gate in front of everything else.

What it is

The assistant is a page you go to, not a panel that follows you. There was a dock — the thread rendered beside every other module, with a button in the top bar — and it was removed: it put one module's chrome into every screen in the product. Nothing in the app draws one.

What survived the removal is the part that was never about the panel. Because the page runs inside the shell, the assistant can answer "what is the operator looking at" with the actual screen, and it can move the app — and both work from its own page. Everything that would change account data stops at a manual approval batch on the server first.

Installed as

Wizard idcoworker
Install itnpx @chatfuel/wizard --modules coworker
Selected byDefault — --yes installs it
RequiresNothing, beyond the implicit core
Recommendslivechat
Skillchatfuel-coworker

The module is neither hidden nor railHidden: it takes an ordinary rail item, in the Growth group beside Publishing and Ads Optimization.

Routes and views

One path, one parameter. There is no view segment — everything the module shows is one thread.

PathWhat it shows
/coworkerThe conversation list on the left, the open thread beside it, the composer under it.
ParameterWhat it does
cThe conversation on screen. Absent means the newest, or an empty page on a fresh bot.

That one parameter still goes through a codec, and the surface reads it on every render rather than seeding state at mount. The assistant's own navigate writes the URL, and a mount-seeded surface would ignore it in silence.

The model

It reads the screen by asking, and it blocks while it waits. The server sends CoworkerFrontendStateRequested and waits a few seconds for coworkerConversationFrontendStateSubmitReply. Answer from something already alive: on a cold page whose first module is still loading, the reply misses the window and the tool comes back {"success":false,…}. The reply is a Map! and not a string map — nested objects, arrays, numbers and booleans all reach the model verbatim, so send structure rather than flattening it. Exactly one client may answer a given requestID; a second one gets FrontendStateRequestNotFound.

It navigates by page name, never by URL. CoworkerFrontendAction {actionType: 'navigate', parameters: {pathKey: 'Deals'}} is a named destination, and the vocabulary is the Chatfuel dashboard's page list rather than this app's — Live Chat, Contacts, Leads, Calendar, Flows, Billing, channel settings, automations, catalog, FAQ, API, teammates — several of which this shell calls something else and several of which it does not have. So lib/shellBridge.ts resolves a name against the module registry (title first, then id) with a sixteen-entry alias table behind it, and builds the URL itself. A name that resolves to nothing is not an error; it is a page this product does not include, and the thread says so.

A route change is reversible, which is why executing one needs no approval and why undo is always offered. What guards it instead is when: an action runs only if it arrived on the live socket, in the conversation on screen, has not run before, is not inside a two-second cooldown, and is not landing while somebody is typing — unless they asked in the last two minutes, which is the case the typing guard was built to allow. Every refusal shows a label instead of a silent no-op: Already done, Was opened earlier, Waiting for you to open this chat, Ready when you are. The history rule is the one that ships as a bug if it is an afterthought: a frontend action is a message, so every page of history contains every navigation the assistant ever performed.

Every tool call costs two messages, and both have empty content. The request carries toolCalls: []; the result carries one entry. Failed results and rejections arrive as fully empty messages — no content, no tool calls. Filtering on "has no content" therefore hides the whole run. The rule that works is the one the module ships: no content and no tool calls is noise; a message carrying tool calls is a step.

Approval is conversation state rather than a message. pendingAction holds the whole batch, one boolean resolves all of it, and none of it runs until you answer. Reads never reach the gate — chatfuel_gql-list_catalog and chatfuel_gql-list_specialists run with no pendingAction at all. There is no expiry: a pending batch blocks the conversation until somebody decides. Sending an ordinary message while one is pending is a rejection, with that text as the denial message.

The module classifies each tool by its leading verb — fifteen read verbs, eleven destroy verbs, everything else a write — and an unrecognised verb or an unknown tool id counts as a write. That is the safe direction to be wrong in.

The agent loop is rate-limited per user account, and past the limit a reply may simply never arrive: the mutation already returned success, so a success is not proof the loop started. isAgentLoopActive is a hint rather than a guarantee — it can still read true after a loop has ended — so "still thinking" is not evidence either. Keep a timeout of your own. The module treats fifteen seconds without an event as a reason to refetch, and two minutes as a reason to stop showing a typing indicator and say the loop is stuck.

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
BotViewCreating conversations, chatting with the assistant, subscribing to updates.

One permission, and it is the whole list. Everything conversation-scoped is ownership-checked on top of it: conversations are per (user account, bot), and another operator's id answers CoworkerConversationDoesNotExist rather than a permission error.

Limits

The conversation list has to name its bot. botID is optional on currentUser.coworkerConversationsConnection, and left out the field lists every bot the deploying account holds — so behind the auth gate the proxy refuses that form with 403 AccountScopeBlocked, along with the form that passes a variable arriving null. Always send it. Reading one thread by id is allowed and checked by the same fence that covers every other in-bot id.

Pagination cannot be trusted past the first page. Do not page on hasNextPage: page until you receive fewer edges than you asked for. A cursor pointing at a row that has since been deleted no longer resolves; restart from the top. Thirty conversations and fifty messages a page.

The unread counter is approximate. It increments on every assistant message, invisible tool messages included, and resets all-or-nothing.

Attachments are images and documents only. At most fifteen files a message, fifty megabytes each; png, jpeg, webp, gif, and PDF, Office and text documents. Video is refused outright — the assistant cannot read it. Audio is not an attachment at all: a voice note takes its own mutation, one file per message. Message attachments are server-side copies and are not kept forever.

A conversation cannot be renamed, deleted or archived. None of the three exists in the public API. The server generates the title from the first user message, so it is a whole sentence, it can be long, and it is sometimes the literal string "null". What the module does instead is keep a title and a pin in frontendStateStorage — server-persisted, so a rename at the desk is a rename on the laptop. Worth knowing: the assistant can read that map. It is the agent's own scratchpad, which is why the keys are plain words.

One tool call in flight shows no spinner. The wire never says which call is running, and with a single step there is no run group to hang the state on. The typing indicator and the composer's Stop cover it.

Four things in the API look usable and are not: ice breakers — both coworkerConversationCreateIceBreakers and coworkerConversationClickIceBreaker — are not usable from this API, coworkerIncognitoConversationCreate is not yours to call, the WhatsApp-proxy plumbing is undocumented here, and the skill-scoped creates need ids this API does not list.

The module's own ceilings:

Conversations per page30
Messages per page50
Files in one message15
One file50 MB
Conversation title kept in the rail52 characters
Params the assistant may put on a URL12 keys, 200 characters each
Navigation cooldown2 seconds
"They just asked" window2 minutes
Refetch after silence15 seconds
Loop declared stuck2 minutes

On this page