Generated operations
The eleven generated document families, what each covers, how the names are formed, and what happens when you add an operation.
Codegen runs one entry per module, over that module's own operations.graphql, so identical
fragment names repeated across modules never collide in a single run. Which modules those are is read
off the disk rather than listed: three of the fourteen ship no operations at all, and a list would be
a fourth copy of a fact the directory already answers.
The eleven families
| Family | Import | Read by | What it covers |
|---|---|---|---|
core | ~api/generated/core/graphql | The shell, and every module | Identity, the bot and workspace lists, the role-and-permission read, file readback, per-user storage, the preview pair a test chat starts from, and the bot's connected channels with their connection and access-refresh links. 18 operations, no subscription. |
livechat | ~api/generated/livechat/graphql | livechat (Inbox) | The conversation list and one thread's messages, sends for each channel, read state, take-over, assignment, notes and WhatsApp templates. 46 operations, 5 of them subscriptions. |
contacts | ~api/generated/contacts/graphql | contacts | Contact lists and counts, the bot attributes catalog, per-contact attribute edits, assignment, stage, and CSV import and export as background tasks. 40 operations, 4 subscriptions. |
deals | ~api/generated/deals/graphql | deals | A deal is a contact carrying a salesStageV2 value: board columns, totals, stage moves, field edits, the table view and its exports. 28 operations, 5 subscriptions. |
knowledge-base | ~api/generated/knowledge-base/graphql | knowledge-base | The structured record under bot.fuelyConfig.knowledgeBase, FAQs, the goods catalog, specialists, and the read-only conversation scan the Gaps source runs. 23 operations, and no subscription — there is none in the schema to have. |
coworker | ~api/generated/coworker/graphql | coworker (Copilot) | The operator-facing assistant chat: conversations per user and bot, sends that return before the reply arrives, tool approvals, streaming stop. 16 operations, 2 subscriptions. |
bookings | ~api/generated/bookings/graphql | bookings | Appointments over bookingsV2, availability, staff and their Google Calendar links, the services catalog, the booking config and the bot time zone. 37 operations, 4 subscriptions. |
automations | ~api/generated/automations/graphql | automations (AI agent) | How the AI behaves, one automation per scope: the settings, their inherit-from-parent twins, the media and ad pickers, and the preview conversation. 49 operations, 3 subscriptions. |
flow-builder | ~api/generated/flow-builder/graphql | flow-builder | Flows, groups, blocks, block elements, buttons and connections. 233 operations, 224 of them mutations, because every element type has its own add mutation. |
ads-optimization | ~api/generated/ads-optimization/graphql | ads-optimization | Event sets over the one automation scope that carries them, their ad lists and event lists, and the inheritance between a custom set and the base one. 14 operations. |
publishing | ~api/generated/publishing/graphql | publishing | Account state, the media library, four publish mutations and the connect URL. 11 operations. |
Counts are top-level definitions in each operations.graphql — 515 operations and 109 fragments
across the eleven. Outside the app — a Node script, the wizard — the same files are reachable as
@chatfuel/api-client/generated/<family>.
admin, auth and channels have no family. auth talks to Supabase over RPC; admin goes
through the proxy's own admin routes; channels reads and writes through core, which is where its
operations live.
A module may import its own family or core, and nothing else. Validate pass 10 fails the build on
a crossing import, which is what keeps one module's screen from quietly depending on another
module's schema knowledge.
How the names are formed
documentMode: 'string' means every document is a printed TypedDocumentString, not an AST. An
operation becomes <OperationName>Document and a fragment becomes <Name>FragmentDoc:
query InstagramAccountState is exported as InstagramAccountStateDocument, and
fragment InstagramAccountRef as InstagramAccountRefFragmentDoc.
The string mode was chosen for size. Every AST mode inlines each fragment's object literal into
every operation that spreads it — a 5.5 MB chunk no bundler can dedupe. String mode has the same
problem in text: codegen prints an operation's whole transitive fragment closure into that
operation's string, and ElementParts alone came out 153 times, three quarters of a 1.9 MB file. So
a post-codegen pass prints each fragment once per file and interpolates the constant instead. The
operation stays first in the text, which is the only thing getDocMeta reads it for, and each
fragment appears exactly once per operation so no server ever sees two definitions of one name. The
pass is idempotent: running it twice changes nothing.
You never name the generated types in module code. Import the document, hand it to the client, and the result type comes with it:
import { InstagramAccountStateDocument } from '~api/generated/publishing/graphql'; useEffect(() => {
let live = true;
setGate({ state: 'loading' });
client
.query(InstagramAccountStateDocument, { botID: botId })
.then((data) => {
if (!live) return;
const scope = data.bot.contactScopes.find(
(candidate): candidate is Extract<typeof candidate, { instagramAccount: Account }> =>
'instagramAccount' in candidate && Boolean(candidate.instagramAccount),
);
const account = scope?.instagramAccount ?? null;
if (!account) setGate({ state: 'absent' });
else setGate(canPublish(account) ? { state: 'ready', account } : { state: 'unpermitted', account });
})
.catch((err: unknown) => {
if (!live) return;
setGate({ state: 'error', message: errorMessage(err) });
});
return () => {
live = false;
};
}, [client, botId, tick, refreshToken]);What the generator emits
The generator body is content/codegen/, and it is copied into every scaffolded app as
scripts/codegen/ so both ends run the same one. Nothing in it imports @graphql-codegen/* — the
config shape is written out by hand in types.ts, because an app that never extends the API still
runs tsc over its scripts/ directory and would otherwise go red on a package it has no reason to
install. This repository is where the two are held together: content/api-client/codegen.ts assigns
the built config to a real CodegenConfig, so the type check fails the day the hand-written shape
stops being one.
The schema everything is checked against is content/schema/schema.graphql — 6,896 lines, generated
from a larger one, names and types only, with no descriptions and no deprecated members.
possible-types.json sits beside it, holding each interface's and union's concrete types.
Adding an operation in your app
A scaffolded app carries the same three inputs under src/vendor/, and regenerates from them
without leaving the repository.
| Path | What it is |
|---|---|
src/vendor/schema/schema.graphql | The SDL everything is checked against. |
src/vendor/api/operations/<module>.graphql | The documents, one file per module. These are the inputs you edit. |
src/vendor/api/generated/ | The output: typed documents, one namespace per module. |
codegen.ts | The config, at the root of the app. |
scripts/codegen/ | The generator: the config body, the fragment hoisting, the stamp. |
- Edit
src/vendor/api/operations/<module>.graphql. - Run
npm run codegen. The first run prints the one command that installs the toolchain — filled in for whichever package manager wrote your lockfile, versions exact — and stops. - Commit the regenerated files under
src/vendor/api/generated/together with the document.
Then npm run check. Never edit src/vendor/api/generated/ by hand: it is output, and the next run
overwrites the edit with no trace. Regenerating an app you have not touched leaves an empty diff, so
a diff you did not expect is a real difference in the inputs.
src/vendor/api/generated/.codegen-inputs.json is the stamp — the digest of the schema and of every
operation document as they were when the client beside them was generated. It answers "are these
types current?" without regenerating to find out. update refreshes the inputs but never the
generated files, so when it moves one it says so under regenerate the client and leaves the run to
you.
A new operation also has to reach src/operationDocs.ts. The proxy builds its registry from the
namespaces that barrel exports and refuses everything else with 403 OperationNotInRegistry, so an
operation you generated into a namespace nothing imports is refused on the first request — a
refusal, not a type error.
Adding one here
Edit content/modules/<id>/skill/examples/operations.graphql — that file is the source document, not
the generated one — then run pnpm codegen at the repository root, which runs graphql-codegen and
the fragment hoist. pnpm codegen:check re-runs it and fails on any diff. pnpm validate is what
checks the result.
| Gate | What it fails on |
|---|---|
| Pass 0, documents | An operation that does not validate against the bundled schema, with the line it is on. |
| Pass 3, name collisions | Two operations anywhere with the same name. Fragments may repeat across families only with byte-identical bodies — which is why bookings prefixes its specialist and service operations even though knowledge-base reaches the same entities. |
| Pass 4, possible types | possible-types.json is re-derived from the SDL and compared. |
| Pass 4c, schema reachability | A declaration in the SDL that no operation can reach from Query, Mutation or Subscription — a surface described with no way in. |
| Pass 9, codegen coverage | A module with operations and no generated namespace, a generated namespace with no operations behind it, or a ./generated/* export map that does not match the directory. A module with no Chatfuel operations at all — auth, admin, channels — is fine and stays out. |
| Pass 10, import boundaries | A module importing a generated family that is neither its own nor core. |
| Pass 17, operation allowlist | The proxy's allowedOperations.ts re-derived from the same documents. A module that gained an operation and a list that did not is a feature that 403s in a deployment with the fence on. |
| Pass 17b, operation docs | content/shell/src/operationDocs.ts must name every generated namespace, or that module's every request is refused. |
| Pass 18, markdown GraphQL | A GraphQL name in a shipped markdown block that the SDL does not have — the reference pages an agent reads before it writes a query. |
The same operations.graphql is copied into a scaffolded app as part of the module's agent skill,
so the coding agent working in that app reads the validated operations rather than guessing at the
schema. What runs them is the client the module receives.
What a module receives
The ModuleClient a module root is handed by props, the options createChatfuelClient takes, and the helpers that ship in the same barrel.
The Chatfuel API
The mental model behind the GraphQL API a generated app talks to — the hierarchy every operation traverses, the two automation surfaces that are not the same thing, and why the schema ships as a file.