A template for building Total CMS extensions. Clone this repo, rename it, and start building.
This starter demonstrates every extension point:
| Feature | File | Description |
|---|---|---|
| Twig function | Extension.php |
starter_greet() function |
| Twig filter | Extension.php |
reverse_words filter |
| Twig global | Extension.php |
Example in register() (commented out) |
| CLI command | src/Command/GreetCommand.php |
tcms acme:greet |
| API route | src/Action/ApiHelloAction.php |
Protected API at /ext/acme/starter/api/hello |
| Public route | src/Action/PublicStatusAction.php |
Unauthenticated at /ext/acme/starter/status |
| Admin page | src/Action/DashboardAction.php |
Admin page at /admin/ext/acme/starter/dashboard |
| Admin nav | Extension.php |
Sidebar link with custom SVG icon |
| Dashboard widget | templates/widgets/starter.twig |
Widget on admin home |
| Event listener | Extension.php |
Logs object create/update events |
| Automation | Extension.php |
prune-drafts scheduled job (+ commented event-driven example) |
| Custom field type | src/Field/ColorPickerField.php + assets/admin.js |
Native HTML color picker field: PHP renders it, the JS class registered through window.TotalCMS.registerFieldType (3.6+) gives it browser behaviour |
| MCP tool | Extension.php |
acme_search_inventory — query the inventory from an AI agent |
| MCP tool with progress | Extension.php |
Bulk-reindex example with SSE progress notifications (commented) |
| MCP resource | Extension.php |
acme://message/of-the-day — addressable content for AI agents |
| MCP resource template | Extension.php |
URI template acme://inventory/{sku} for unbounded resource sets (commented) |
| Admin CSS asset | assets/colorpicker.css |
Styles for the color picker field |
| Admin JS asset | assets/admin.js |
The color picker's JavaScript class, registered with the form factory |
| Frontend CSS/JS assets | Extension.php |
Examples in register() (commented out) |
| Container definition | Extension.php |
Example in register() (commented out) |
| Page middleware | Extension.php |
Example in register() (commented out) |
| Read-only schema | schemas/starter-items.json |
Example collection schema (Pro+) |
| Installable schema | Extension.php |
Example in boot() (commented out, Pro+) |
| Settings | settings-schema.json |
Configurable greeting message |
| Icon | icon.svg |
Displayed on the extension card in the admin UI |
| Manifest links | extension.json |
"Documentation" and "Dashboard" links on the admin card |
| Review note | extension.json |
Plain-language note shown on the pre-enable review screen |
| Agent skill | skill/SKILL.md |
Teaches Claude Code and other coding agents how to use the extension; installed to .claude/skills/acme-starter/ while enabled (3.6+) |
There are two ways to install an extension. On a Composer install of Total CMS (3.6+) the extension can be a Composer package, which is how you will want to distribute it; while developing, cloning into the extensions directory is quicker.
As a Composer package. This repo's composer.json already has "type": "totalcms-extension", which is what Total CMS looks for. Publish it to Packagist (or a private repository listed in the site's composer.json) and operators install it with:
composer require your-vendor/your-extension
tcms extension:enable your-vendor/your-extensionComposer autoloads your src/ classes, composer update moves the extension to new versions, and composer remove takes it out. The package name and the id in extension.json may differ; the id is what the admin, the CLI and the state files use.
Cloned into the extensions directory.
- Clone this repo into your T3 extensions directory:
cd tcms-data/extensions/
mkdir your-vendor
cd your-vendor
git clone https://github.com/totalcms/extension-starter.git your-extension
cd your-extension- Update
extension.jsonwith your extension's ID, name, and details:
{
"id": "your-vendor/your-extension",
"name": "Your Extension"
}- Update the PHP namespace in
composer.jsonand all PHP files:
"autoload": {
"psr-4": {
"YourVendor\\YourExtension\\": "src/"
}
}- Install dependencies and generate the autoloader:
composer install- Enable the extension:
tcms extension:enable your-vendor/your-extension- Delete the parts you don't need and start building.
your-extension/
extension.json # Manifest (required)
Extension.php # Entry point (required)
icon.svg # Extension icon (optional)
composer.json # Dependencies and autoloading
settings-schema.json # Settings form definition
src/
Action/ # HTTP action handlers
Command/ # CLI commands
schemas/ # Read-only schemas (Pro+)
skill/ # Agent skill: SKILL.md + optional references/
templates/ # Twig templates
Add a links array to extension.json to surface buttons on the admin Extensions card:
"links": [
{"label": "Documentation", "url": "https://docs.totalcms.co/extensions/"},
{"label": "Dashboard", "url": "/admin/ext/acme/starter/dashboard"}
]Each entry needs a label and a url.
- URLs starting with
http://orhttps://are treated as external and open in a new tab. - Relative URLs (admin pages your extension registers) only show when the extension is enabled — they wouldn't resolve otherwise. External links are always shown so users can read your docs before enabling.
When a user enables an extension that touches sensitive capabilities — public (unauthenticated) routes, listening to all content changes, registering services, or exposing tools/resources to AI agents over MCP — Total CMS shows a short pre-enable review screen before the extension turns on. (Extensions that use none of those just enable in one click.) The screen also lists any high-risk patterns a static source scan finds in your code (shell_exec, raw network calls, etc.).
The reviewNote field in extension.json is your message at the top of that screen — your chance to explain, in plain language, what your extension does and why it needs the access it asks for:
"reviewNote": "Receives Stripe webhooks at a public endpoint to confirm payments, and watches new orders to send notifications. No data leaves your site except the Stripe calls you configure."Guidelines:
- Write for the site owner, not a developer. Say what the extension does and why it needs each sensitive capability.
- Be specific about public endpoints and data access — those are what users worry about.
- Keep it short — a sentence or two. It's a reassurance, not a manual.
- You'll see this exact screen when you enable your own extension during testing, so you can preview how it reads.
Two behaviors worth knowing as you build:
- Capabilities you add in a later version default to off for users who already had your extension enabled. Total CMS surfaces the new capability in the extension's settings as a toggle, so the user opts in deliberately — adding a capability in an update won't silently start using it.
- An update that introduces high-risk code patterns (caught by the source scan) disables the extension until the user reviews and re-enables it. Clean updates install without interruption. So keep risky calls intentional and explainable.
reviewNote is optional — but on an extension that uses any sensitive capability, a good note is the difference between a confident "Enable" and a nervous one.
skill/SKILL.md teaches coding agents how to use your extension. While the extension is enabled, tcms skill:install copies the skill/ directory to .claude/skills/{vendor}-{name}/ in the site's project, next to Total CMS's own skill, and Claude Code and other agents that read .claude/skills/ pick it up by its description line. Enabling or disabling the extension installs or removes the folder at once; on a Composer install every composer update refreshes it. Requires Total CMS 3.6.
Write it for an agent that has never seen your extension:
- What it adds and when to reach for each piece — the Twig functions and filters, CLI commands, routes, field types, settings keys, schemas. The table in this repo's
SKILL.mdis a good shape. - The mistakes an agent is likely to make — calling a function while the extension is disabled, hard-coding a value that lives in a setting, editing a read-only schema.
- Paths for the Composer layout (
vendor/bin/tcms). They are rewritten for zip installs on copy. - Frontmatter
nameanddescription. The description is what an agent matches on, so say what the extension is for, not just its name.
A skill is instructions to an agent, so the pre-enable review screen shows its full text to the site owner next to your review note and the source scan. Write it to be read by them too. Keep SKILL.md short and put long reference material under skill/references/; there is no capability to declare and no permission toggle.
Two ways to ship schemas with an extension. Both require the Pro edition.
| Approach | Where it lives | Editable by user? | Use when |
|---|---|---|---|
| Read-only | schemas/yourname.json in the extension |
No — owned by the extension | The schema is core to the extension's behavior and shouldn't be modified |
| Installable | $context->installSchema([...]) in boot() |
Yes — copied once into tcms-data/.schemas/ |
The schema is a starting point the user is expected to customize |
Read-only schemas are auto-discovered. They show up in the admin schemas view alongside built-in schemas, and can be referenced by collections. Disabling the extension's Schemas capability hides them.
installSchema() is a one-shot copy: it skips silently if a schema with the same id already exists, so subsequent boots don't overwrite the user's edits.
The requires block in extension.json is enforced before the extension can be enabled:
"requires": {
"totalcms": ">=3.3.0",
"php": ">=8.2"
}If the running Total CMS or PHP version doesn't satisfy these constraints, the extension is still listed on the Extensions admin page but the Enable button is disabled and a yellow warning panel explains why. This means users can see your extension exists and read its docs, but they can't enable it on an unsupported runtime.
MIT