Skip to content
ChatfuelSDK
Reference

The shell API

The two hooks a module can reach beyond its props, what the shell does with what they publish, and the three files behind them that a module may not import.

Beyond its props, a module can reach two hooks and their types, and nothing else.

What a module can call

Everything below is exported from apps/shell/src/modules/shellApi.ts, the one runtime file on the module import allowlist.

ExportKindFor
usePublishScreenContext(detail)HookPublish what the operator is looking at.
useShellBridge()HookRead or move the app around this module. Returns null in an embed.
ScreenValue, ScreenDetailTypesWhat you may publish.
ScreenSnapshotTypeWhat the bridge answers with.
ShellAction, ShellActionResult, ShellBridgeTypesThe bridge's own shapes.
ScreenContextProvider, ShellBridgeProviderComponentsThe shell's, not yours. It mounts them around the module.

Neither hook lets a module reach another module. Publishing is write-only into a sink the shell owns, and the bridge only ever resolves against the module registry.

Both are no-ops in an embed. There is no provider above the module, so usePublishScreenContext publishes into a sink that discards, and useShellBridge() answers null — a module mounted inside somebody else's product has no business reading or rewriting their address bar. Handle the null; the one caller in the tree degrades to saying it could not move rather than pretending it did.

Publishing what the operator is looking at

usePublishScreenContext(detail: ScreenDetail | null): void

ScreenDetail is Record<string, ScreenValue>, and ScreenValue is the whole JSON grammar — strings, numbers, booleans, null, arrays, nested objects. Not a flat string map: the live pass proved the assistant's submit mutation round-trips nested structures to the model verbatim, so flattening would only lose information.

Call it wherever the answer actually lives — the workspace that knows the view, or the view that knows the row count — as many times as you like. The shell merges every live entry, keyed by useId(), and a later mount wins a key clash, which is the right precedence: the view knows more than the workspace hosting it. Pass null to publish nothing.

The value is keyed by its own JSON, so an inline object literal is fine and needs no memoising — re-publishing happens only when the content changes.

Five modules publish today: livechat, deals, bookings, flow-builder and automations.

Reading and moving the app

useShellBridge(): ShellBridge | null

ShellBridge has two methods.

snapshot(): ScreenSnapshot answers { moduleId, moduleTitle, url, params, detail, destinations } — the address bar verbatim (the model quotes it back and it has to be real), this module's params, the merged detail from every live publish, and where else the app could be sent. It is read at call time through a ref, so a snapshot taken ten minutes after the bridge was built is still current.

run(action: ShellAction): ShellActionResult takes { actionType, parameters } — free strings on the wire, because the vocabulary is the server's — and answers { ok, label, undo? }. Four outcomes:

Outcomeoklabel
actionType is not navigatefalseI don't know how to "<actionType>" in this dashboard
The pathKey resolves to nothingfalseThere is no "<name>" page here, or That navigation had no destination when it was not a string
Already at that URLtrueAlready on <title>
MovedtrueOpened <title>, with undo restoring the address it left

undo is always available on a successful move, because everything the bridge can do is a route change and a route change is reversible — which is also why executing one needs no approval gate. Anything that changes account data is a server-side tool and goes through the server's own manual-approval batch instead.

The vocabulary run accepts

pathKey is a named destination, never a URL. resolveDestination normalises both sides (lowercase, non-alphanumerics stripped) and matches title first, then id, then an alias table — title first because the model has only ever seen the product's page names, id because that is what somebody writing the parameters by hand reaches for. First match in registry order wins, which is the order the rail shows.

AliasModule
livechat, inbox, chats, conversationslivechat
leads, pipelinedeals
calendar, appointmentsbookings
flows, flowbuilderflow-builder
catalog, faq, businessinfoknowledge-base
channelsettings, aisetupautomations
assistantcoworker

Those are Chatfuel's own page names for things this shell calls something else. Names the model uses that exist in neither the table nor the registry — Billing, API, teammates — resolve to nothing and are reported as not being in this dashboard, which is the truth.

parameters.params becomes the URL query, capped on both axes: at most 12 keys, each value truncated to 200 characters. Objects, arrays, null and undefined are dropped rather than stringified. This is model output landing in the address bar and a module reads it as its own deep link.

The shell's own half

Three files implement all of the above. None of them is on the import allowlist, so a module that reaches for one fails the boundary pass rather than compiling.

FileExportsWhat it is
apps/shell/src/lib/screenContext.tsScreenSink, createScreenSink()The sink behind the hook: publish, read, size. Deliberately not React state — publishing happens on every filter change and every page of rows, and nobody re-renders when it does. It is a plain Map the shell holds in a ref, and read() is called at the one moment the assistant's tool fires.
apps/shell/src/lib/shellBridge.tsDestination, resolveDestination, actionParams, MAX_ACTION_PARAMS, MAX_ACTION_PARAM_LENGTH, BridgeDeps, createShellBridgeThe bridge itself. createShellBridge takes the destinations, a currentRoute() and currentUrl() read at call time, the shell's own navigate and restore, and readDetail().
apps/shell/src/lib/botSelection.tsWorkspaceOption, Selection, resolveSelection, workspaceOptions, readStoredSelection, writeStoredSelectionWhich workspace and which bot the app is looking at. Nothing to do with modules — the topbar's, and React-free so the rules can be tested on their own.

botSelection answers from three sources in this order of authority: what the person last chose (localStorage, under chatfuel.workspace and chatfuel.bot), what the wizard wrote as the starting point (VITE_CHATFUEL_WORKSPACE_ID), and what the account actually has right now — the last being the referee, so a stored id whose bot has since been deleted is a stale note to drop rather than an error to show. With no stored workspace, the remembered bot names where it lives now, because a bot can be moved between workspaces and following it is the more faithful restore.

workspaceOptions exists because Chatfuel does not require workspace titles to be unique and makes them collide by itself: creating a bot without naming a workspace leaves a throwaway "My Workspace" behind. Same-named entries get their bots appended, and identical-all-the-way-down ones get the last six characters of the id.

Both localStorage calls are wrapped: a browser with site data switched off throws on the property access itself, and losing the memory of a choice is not worth a blank screen.

The third file on the allowlist, apps/shell/src/modules/testClient.ts, is not an API at all — it is an inert ModuleClient for the render smoke tests, where effects never run. A request made from it never answers, rather than answering with a lie.

On this page