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. The module's own manifest and the repo's module table still describe that dock; both are stale, and 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 about ten seconds for coworkerConversationFrontendStateSubmitReply. Answer from something already alive: on a cold page whose first module was still loading, the reply missed the window and the tool came 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. Asked which names it accepts, the assistant answered with Chatfuel's own page names — 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 ran 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 to roughly 100 starts a minute per user account, and going over it fails silently: the mutation already returned success and no reply ever arrives. isAgentLoopActive is Redis-backed with a two-minute TTL and can stay true after a server crash, so "still thinking" is not evidence either. 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

Pagination cannot be trusted past the first page. hasNextPage is wrong after it — an off-by-one on the server — so page until you receive fewer edges than you asked for. A cursor pointing at a deleted row answers InternalServerError; 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 with a roughly four-month lifetime.

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: coworkerConversationCreateIceBreakers always returns [], coworkerConversationClickIceBreaker panics the server, coworkerIncognitoConversationCreate is for Chatfuel employees, and the skill-scoped creates need ids that are not listable publicly.

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