extension.json Reference
Every field the extension manifest accepts.
The full manifest
The manifest is parsed into the ExtensionManifest struct in internal/cms/extension_loader.go. The kernel is forgiving with the file: missing optional fields are silently skipped, unknown fields are ignored, and extras don't cause the extension to fail to load. That tolerance is deliberate — it's also why mistakes are easy to miss. Run core.extension.standards after editing the manifest to audit it against the recommended shape and catch typos and orphaned references.
Required fields
name(string) — display name, e.g. "Forms".slug(string) — kebab-case identifier; must equal the directory name underextensions/. The loader will fall back to the directory name ifslugis omitted, but explicit is better than implicit.version(string) — semver, e.g. "2.0.0". Used in plugin handshake metadata and surfaced in admin lists.
Everything else is optional. The minimal extension is three lines of JSON in a directory.
{
"name": "Hello",
"slug": "hello",
"version": "0.1.0"
}
Identity and lifecycle
author(string) — displayed in the admin extensions list.description(string) — free-form summary surfaced in the admin.priority(int, default 50) — execution order across extensions. Lower runs first. Use it to enforce ordering between extensions that subscribe to the same event.provides(string[]) — capabilities this extension advertises, e.g.["media-provider"]or["email.provider"]. The kernel uses these tags to resolve provider lookups:core.media.uploadroutes to whichever active extension declaresmedia-provider;core.email.sendroutes to whichever declaresemail.provider. If two are active, the higher-priority extension wins.auto_activate(bool) — when true, the row is created withis_active=trueon the first install. The flag is ignored on existing installs (theis_activecolumn is not in the upsert update set, so user choices stick). This replaces the old kernel-side hardcoded list of "core" extensions.
Capability and ownership fields
capabilities(string[]) — the CoreAPI permissions this extension may use. Calls outside the declared set returnpermission denied— there is no override flag.data_owned_tables(string[]) — table names this extension owns. The data-store CoreAPI rejects reads/writes on tables that aren't owned (or are kernel-private). Migrations create these tables; the loader does not auto-derive ownership from migration content, so you must list every owned table explicitly.
gRPC plugin fields
plugins is an array of {binary, events} entries. Each entry launches one plugin process. Most extensions ship a single plugin (or none).
binary(string) — path to the executable, relative to the extension directory. Conventionallybin/<slug>.events(string[]) — declarative subscriptions for this plugin. Equivalent to returning the same names fromGetSubscriptions()at runtime. Wildcards (*,node.*) are supported.
Empty or missing plugins means the extension has no compiled binary — it's Tengo-only or UI-only.
Routing fields
public_routes is an array of {method, path}. Method is uppercased automatically. The wildcard * at the end of a path captures the rest. Reserved path roots are rejected with a warning at registration time — see the Extensions Overview for the full list. Admin proxy routes do not need declaration: anything under /admin/api/ext/<slug>/* is auto-proxied to the plugin's HandleHTTPRequest.
admin_routes is an array of {method, path, required_capability}. The proxy enforces the capability gate before forwarding to the plugin so a logged-in admin without the right cap cannot bypass UI guards by POSTing directly. Without this list, extensions are gated only by admin_access — fine for read-only extensions, a hole for anything mutating data the user shouldn't touch.
Admin UI fields
If your extension ships a micro-frontend, declare it under admin_ui. The loader does not validate the path — a typo silently breaks the UI — so verify the build output exists at the path you reference.
"admin_ui": {
"entry": "admin-ui/dist/index.js",
"routes": [
{ "path": "/admin/forms", "component": "FormsListPage" },
{ "path": "/admin/forms/:id", "component": "FormEditorPage" }
],
"menu": {
"label": "Forms",
"icon": "FormInput",
"section": "Content",
"position": "30",
"path": "/admin/forms"
},
"slots": { "node-editor.sidebar.bottom": { "component": "FormPickerSlot", "label": "Linked forms" } },
"field_types": [ { "type": "vibe-form", "label": "Form picker", "component": "VibeFormFieldInput", "icon": "FormInput", "group": "Relational" } ]
}
entry— relative path to the built ES module. Required ifadmin_uiis present.routes— admin-side React routes.componentmust be a named export fromentry. Routes use the same path-pattern grammar as React Router.menu— top-level sidebar entry.sectionplaces it under one of the SPA shell's known sections (Content,Design,Development,Settings);positionorders within the section. Usechildrenfor sub-items.slots— named injection points the SPA shell exposes (e.g.node-editor.sidebar.bottom,node-editor.toolbar.left). Slot keys are hard-coded by the shell; an unknown key is silently ignored.field_types— custom field-type registrations. Once registered, you can usetype: "vibe-form"in any node-type or block field schema and the editor will mount the declared React component.
Settings fields
Two surfaces, used for different purposes:
settings_schema(map of name →SettingsField) — the lightweight per-extension settings the admin can edit from the extension's row. Each field is{type, label, required, default, sensitive, enum}. Sensitive fields are encrypted at rest using the AES-256 master key.settings(raw JSON array) — the rich, schema-driven settings declaration. Each entry is a complete settings.Schema (sections + fields with translatable flags). Registered into the in-process settings registry on activation, unregistered on deactivation. The schema id is namespaced toext.<slug>.<id>automatically so two extensions can declare schemas with the same local id without collision.
Use settings_schema for a handful of API keys; reach for settings when the extension wants its own settings page with sections, multilingual fields, and gated capabilities.
Theme-asset fields
An extension can ship blocks, layouts, partials, page templates, and assets, exactly like a theme. Image sizes are theme-only — they live on theme.json's image_sizes, not on the extension manifest.
blocks— content block definitions:{slug, label, field_schema, html_template, test_data, icon, block_css, block_js, cache_output}. Either inline the template or reference a file underblocks/<slug>/view.html.layouts/partials— page-layout templates the extension contributes to the active theme.templates— pre-built page block sequences for one-click insertion in the editor.assets— array of{key, src, alt}. Bundled images imported into the media library on activation; subsequent template lookups usetheme-asset:<key>to find them.
Validation
The loader does not fail on a missing optional field — it just silently skips that surface. Unknown fields are ignored. Run core.extension.standards at any point to audit your manifest against the recommended shape, including: orphaned component references, entry paths that don't exist on disk, data_owned_tables entries that aren't actually created by any migration, and capabilities declared but never used. The result is a list of {severity, code, message} entries you can iterate before declaring an extension done.