# module.json

> Every field of a module manifest — the required five, the two enums, the two regex patterns, and what reads each one.

Page: https://sdk.chatfuel.com/docs/reference/module-manifest
Markdown: https://sdk.chatfuel.com/docs/reference/module-manifest.md

Five fields are required — `id`, `name`, `description`, `status`, `skill` — and `additionalProperties`
is `false`, so a field the schema does not know is a failed gate rather than a silent no-op.

## The top level [#the-top-level]

<TypeTable
  type="{
  $schema: {
    description: 'Points at packages/module-kit/module.schema.json for editor completion. Not read by anything.',
    type: 'string',
    default: 'none',
  },
  id: {
    description:
      'The directory name on both sides, and the module’s first path segment. Pattern: ^[a-z][a-z0-9-]*$.',
    type: 'string',
    required: true,
  },
  name: {
    description: 'What the picker calls it.',
    type: 'string',
    required: true,
  },
  description: {
    description:
      'The picker’s hint, and the one line the wizard prints when a build has exactly one module in it.',
    type: 'string',
    required: true,
  },
  status: {
    description: 'The wizard only offers modules with status &#x22;ready&#x22;.',
    type: &#x22;'planned' | 'ready'&#x22;,
    required: true,
  },
  hidden: {
    description:
      'No nav-rail item and no &#x22;/<id>&#x22; route — the module wraps or extends the shell instead. The shell descriptor must carry the matching hidden: true.',
    type: 'boolean',
    default: 'false',
  },
  selection: {
    description: '&#x22;opt-in&#x22; modules are offered in the picker but never auto-included by --yes.',
    type: &#x22;'default' | 'opt-in'&#x22;,
    default: &#x22;'default'&#x22;,
  },
  requires: {
    description: 'Module ids that must be co-installed. &#x22;core&#x22; is implicit for every module and never listed.',
    type: 'string[]',
    default: '[]',
  },
  recommends: {
    description: 'Module ids the wizard suggests; the user can decline.',
    type: 'string[]',
    default: '[]',
  },
  skill: {
    description: 'Where the agent skill comes from and what it installs as. Required.',
    type: '{ installAs: string; dir?: string }',
    required: true,
  },
  app: {
    description:
      'UI contribution to the shell. Omit it for a skill-only module — core has no app block at all.',
    type: '{ env?: EnvEntry[]; embed?: EmbedSpec }',
    default: 'none',
  },
  permissions: {
    description: 'The Chatfuel permissions this module’s surfaces need.',
    type: 'Permission[]',
    default: 'none',
  },
}"
/>

## What `status` and `selection` actually do [#what-status-and-selection-actually-do]

| Value                  | Effect                                                                                                                                                                                                        |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status: "planned"`    | The module is in the tree and validated like every other, and `ready()` filters it out. The picker never offers it, and `--modules` naming it ends the run with the list of ids that are ready.               |
| `status: "ready"`      | Offered.                                                                                                                                                                                                      |
| `selection: "default"` | Taken by `--yes`.                                                                                                                                                                                             |
| `selection: "opt-in"`  | Offered in the picker with an extra hint, and never taken by `--yes` — name it in `--modules` to get it. Two modules use it: `auth` and `admin`, both because they need credentials the wizard cannot invent. |

`requires` is resolved into a closure before anything is written: dependencies first, `core` always,
and the ids it added are printed. `recommends` is only ever *offered* — under `--yes` or `--modules`
it is logged and not installed, and a mutual recommendation between two modules is legal where a
`requires` cycle is not.

## `hidden` [#hidden]

A hidden module contributes no rail item and no `/<id>` route. The flag lives in the manifest as well
as on the shell descriptor because the wizard writes the agent handoff from manifests alone and would
otherwise print a deep link to a route that does not exist — and the two are compared, so they cannot
drift.

`auth` is the only module that sets it: it contributes a host integration instead of a page.
`railHidden`, which keeps the route and only takes the listing away, is a descriptor field with no
manifest counterpart — see [the module contract](/docs/reference/module-contract).

## `skill` [#skill]

<TypeTable
  type="{
  installAs: {
    description:
      'Directory name the skill is installed as, inside the agent’s skills directory. Pattern: ^chatfuel-[a-z0-9-]+$. Must be unique across every module, and must equal the &#x22;name&#x22; in the skill’s SKILL.md frontmatter.',
    type: 'string',
    required: true,
  },
  dir: {
    description: 'Where the skill lives inside modules/<id>/.',
    type: 'string',
    default: &#x22;'skill'&#x22;,
  },
}"
/>

## `app.env` [#appenv]

An array of the variables this module needs. The wizard writes the union across the selected
modules, first declaration wins, preceded by the app's own two (`VITE_APP_NAME`, `VITE_APP_LOGO`),
which belong to no manifest.

<TypeTable
  type="{
  name: {
    description: 'The variable name.',
    type: 'string',
    required: true,
  },
  secret: {
    description:
      'Collected into the handoff’s &#x22;NEVER print or commit&#x22; list. The env writer itself special-cases exactly one name, CHATFUEL_TOKEN, which is filled from the token step.',
    type: 'boolean',
    default: 'false',
  },
  default: {
    description: 'Written when no step resolved a value.',
    type: 'string',
    default: 'none',
  },
  optional: {
    description:
      'May stay empty. With no value the wizard writes it as a commented-out &#x22;# NAME=&#x22; line — an empty string is a value to dotenv, and a set-but-empty var reads as configured.',
    type: 'boolean',
    default: 'false',
  },
  resolve: {
    description:
      'Which wizard step supplies the value: the Supabase auth setup, the Chatfuel workspace picker, or the admin-password step. Documentation only — see below.',
    type: &#x22;'authSetup' | 'workspacePick' | 'adminSetup'&#x22;,
    default: 'none',
  },
  prompt: {
    description: 'Declared by the schema and read by nothing in the wizard.',
    type: 'string',
    default: 'none',
  },
}"
/>

Values resolve in this order: a step-resolved value, then the token for `CHATFUEL_TOKEN`, then the
manifest `default`, then `''`.

`resolve` documents which step fills the variable; no code reads the field. The steps write into the
answer bag **by variable name** — the workspace picker sets `VITE_CHATFUEL_WORKSPACE_ID` and
`CHATFUEL_WORKSPACE_ID`, the auth setup sets the `SUPABASE_*` ones, the admin step sets
`ADMIN_PASSWORD` — so a `resolve` value that is wrong changes nothing, and a variable nobody writes
comes out empty however it is annotated.

<Callout type="warn">
  The TypeScript type beside the schema is one value behind: `ModuleManifest`'s `resolve` union is
  `'authSetup' | 'workspacePick'`, while the schema allows `adminSetup` and `modules/admin/module.json`
  uses it. The schema is the authority and the ajv gate passes; a manifest edited through the
  TypeScript type would not type-check.
</Callout>

## `app.embed` [#appembed]

What `--embed` copies into a host project.

<TypeTable
  type="{
  roots: {
    description:
      'Shell-relative embeddable roots, e.g. &#x22;src/modules/livechat&#x22;. Every one must exist under apps/shell.',
    type: 'string[]',
    required: true,
  },
  entryComponent: {
    description:
      'The component the agent is told to mount. Must actually be exported somewhere under the first root.',
    type: 'string',
    required: true,
  },
  npmDependencies: {
    description:
      'Extra packages this module needs in a host project. The wizard installs the union across the selected modules, and drops an unselected module’s from the scaffold’s package.json. Only auth declares any (@supabase/supabase-js).',
    type: 'Record<string, string>',
    default: 'none',
  },
  playbook: {
    description:
      'Path to the wiring guide inside the skill dir, conventionally &#x22;playbooks/embed.md&#x22;. A ready embeddable module without one fails the gate — embed mode is agent-driven, and the playbook is what the agent follows.',
    type: 'string',
    default: 'none',
  },
}"
/>

## `permissions` [#permissions]

<TypeTable
  type="{
  object: {
    description:
      'The Chatfuel permission object. Free-form in the schema; five values are in use — Bot, Flows, Ai, Inbox, People.',
    type: 'string',
    required: true,
  },
  action: {
    description: 'The action on it — &#x22;View&#x22;, &#x22;Edit&#x22;.',
    type: 'string',
    required: true,
  },
  requiredFor: {
    description: 'What stops working without it. Printed after the permission in the agent handoff.',
    type: 'string',
    default: 'none',
  },
}"
/>

Nothing checks these at runtime. They are read once, when the wizard writes the handoff notes, so the
agent that continues the work knows what the token has to be allowed to do.

## What the gates check [#what-the-gates-check]

`pnpm validate` runs both of these; a failure is a failed build, not a warning.

| Check                                                  | Failure                                                                                                                                                                                                                                                                      |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ajv against the schema                                 | Reported per field path — a bad pattern, a value outside an enum, a property the schema does not have.                                                                                                                                                                       |
| `id` vs the directory                                  | `id "x" does not match directory name "y"`.                                                                                                                                                                                                                                  |
| `skill.installAs`                                      | Already used by another module.                                                                                                                                                                                                                                              |
| `SKILL.md` frontmatter                                 | Its `name` must equal `installAs`.                                                                                                                                                                                                                                           |
| `requires` / `recommends`                              | May not name themselves, or a module that does not exist.                                                                                                                                                                                                                    |
| `requires` cycles                                      | Detected over `requires` edges only.                                                                                                                                                                                                                                         |
| The shell side, for a ready module with an `app` block | `apps/shell/src/modules/<id>/index.tsx` exists and exports `moduleDescriptor` under that exact name; its `hidden` matches the manifest's; the id is in the shell registry; `modules/<id>/handoff.md` exists; every embed root, the playbook and the entry component resolve. |
