Extend the CMS
Add your own code without touching a core file
An extension registers what it provides — Twig functions, CLI commands, admin pages, API routes, field types, event listeners — through a stable context object. A broken one cannot take the site down.
An extension is a folder with a manifest and an entry-point class. It implements ExtensionInterface and talks to Total CMS through an ExtensionContext — a stable, versioned API — in two phases: register() declares what the extension provides, and boot() wires it into the running application once the container is built.
What it can provide covers most of what you would otherwise fork core to do: Twig functions, filters and globals; tcms CLI commands; API, admin and public routes; admin sidebar items and dashboard widgets; custom field types usable in any schema; event listeners on content events; page middleware that Site Builder pages opt into; form actions; MCP prompts and search providers; admin and frontend CSS/JS; container definitions; and scheduled or event-driven automations whose handler is a closure in your code. Each point has its own capability name, and capabilities appear in the admin as permissions an operator can switch off individually without uninstalling anything.
Where an extension lives decides who owns it. Site-specific ones belong in an extensions/ directory at the project root, next to tcms-data/ — that is code, so it belongs in your git repo and deploys with a git pull. Distributed ones install into tcms-data/extensions/{vendor}/{name}/. Total CMS also ships bundled extensions with every install, disabled by default. If the same id exists in more than one place the most specific copy wins, and the shadowing is logged.
Safety is not left to the author. Every place extension code runs during a request is wrapped, so an exception produces nothing instead of a white screen: it is logged to extensions.log, recorded in the extension's state for the admin to show, and every other extension carries on loading. On a production site an extension that crashes repeatedly is auto-quarantined — disabled for you, with a one-click re-enable — while a development site keeps it enabled and loud so you can debug it.
You can also see what each one costs. Total CMS samples request timings and shows the average and most recent cost per extension on the Extensions page, with configurable warnings when one extension, or all of them together, gets heavy. Manage everything from Settings → Extensions or the CLI: tcms extension:list, enable, disable, remove.
What you get
One context, many extension points
Twig functions and filters, CLI commands, routes, admin pages, widgets, field types, event listeners, page middleware, search providers and more — all through ExtensionContext.
Permissions per capability
Each extension point has a capability name, and an operator can switch individual capabilities off from the extension's settings page without uninstalling it.
A broken extension cannot crash the site
Exceptions during register, boot or a request are caught, logged to extensions.log and shown in the admin. Every other extension keeps loading.
Auto-quarantine on production
An extension that crashes repeatedly on a production site is disabled automatically, with a banner and a one-click re-enable. Development sites stay loud instead.
Project extensions live in git
Put a site-specific extension in an extensions/ folder at the project root and it is code you commit and deploy, not content sitting inside tcms-data.
In practice
One snippet
<?php
namespace Acme\Hello;
use TotalCMS\Domain\Extension\ExtensionContext;
use TotalCMS\Domain\Extension\ExtensionInterface;
use Twig\TwigFunction;
class Extension implements ExtensionInterface
{
public function register(ExtensionContext $context): void
{
$context->addTwigFunction(
new TwigFunction('hello', fn (string $name): string => "Hello, {$name}!")
);
}
public function boot(ExtensionContext $context): void
{
// Nothing to do on boot for this one
}
}
That is a whole working extension: a manifest beside it, then {{ hello('World') }} works in every template on the site.
FAQ
Common questions
What happens if an extension has a bug?
The exception is caught and logged to extensions.log, recorded in the extension's state so the admin can show it, and the rest of the site keeps working. On production, an extension that crashes repeatedly is disabled automatically until you re-enable it.
Can I stop an extension doing one particular thing?
Yes. Each extension point has a capability, and capabilities show up as individual permission toggles on the extension's settings page — so you can leave an extension enabled while switching off, say, its automations.
Does an extension need a particular edition?
An extension can declare a min_edition of lite, standard or pro in its manifest, and one requiring more than the site's license is not loaded. Two things are Pro regardless: schemas provided by an extension, and extension-registered form actions.
How do I start writing one?
Clone the extension-starter repository into your extensions directory — it is a working extension demonstrating every extension point — then run tcms extension:enable your-vendor/your-extension.
Related features
- Automations Write a PHP handler, give it a cron schedule, a webhook, or a content event. Run history, guard rails and auto-disable are built in.
- Site Builder Pages live in the builder-pages collection with a route and a template. The router matches URLs at request time, so a page is live the moment you save it.
- Twig templating Plain Twig 3 templates with one cms global for collections, media, feeds and SEO markup. Automatic escaping, compiled templates, and no PHP to write.
Want the details? Read the Extensions documentation →
Or keep browsing: every Total CMS feature →
Start Your Free 45-Day Trial