Assegai's built-in generators help you move quickly through the framework's common workflows.
Custom schematics let you go further: they let you teach the CLI your own architecture, naming rules, and domain patterns.
That means your team can generate commands like:
assegai g loyalty-program rewardsassegai g menu-sync ubereatsassegai g tenant-onboarding enterprise
without forking the CLI or hand-copying files across projects.
This is one of the easiest ways to turn Assegai from "a framework with generators" into "a framework that understands how our team builds software."
Why this matters
Once an application grows beyond a demo, teams usually repeat the same patterns:
- service classes
- DTOs
- provider integrations
- config files
- docs stubs
- support files in TypeScript, JSON, YAML, or Markdown
Without custom schematics, that repetition becomes manual work.
With custom schematics, one command can generate the same structure consistently every time.
That gives you:
- faster setup for repeatable features
- fewer naming mistakes
- less copy-paste drift
- codebases that stay predictable across teams and projects
What a schematic actually is
A schematic is a recipe for generation.
At the simplest level, the recipe defines:
- which arguments and options the command accepts
- which template files to read
- where the generated files should be written
- which placeholder values should be replaced
If that is all you need, a declarative schematic is usually enough.
If your generator needs branching logic, custom path rules, or content assembly in PHP, you can move to a class-backed schematic.
Start with the easiest path
If you are new to custom schematics, start with a declarative schematic.
Generate a starter:
assegai schematic:init loyalty-program
That gives you a folder like this:
schematics/loyalty-program/
schematic.json
templates/
service.php.stub
That is enough for a working custom generator.
A first declarative example
Example manifest:
{
"name": "loyalty-program",
"aliases": ["lp"],
"description": "Generate loyalty program scaffolding.",
"requiresWorkspace": true,
"kind": "declarative",
"arguments": [
{
"name": "name",
"description": "The feature name to generate.",
"required": true
}
],
"options": [
{
"name": "provider",
"description": "The loyalty provider name.",
"acceptValue": true,
"valueRequired": true,
"default": "internal"
}
],
"templates": [
{
"source": "templates/service.php.stub",
"target": "__SOURCE_ROOT__/__NAME__/__NAME__Service.php"
}
]
}
Example template:
<?php
namespace __CURRENT_NAMESPACE__;
class __NAME__Service
{
public string $provider = '__OPTION_PROVIDER__';
}
Run it:
assegai g loyalty-program rewards --provider=partner-plus
Result:
src/Rewards/RewardsService.php
The generated file will contain:
<?php
namespace Assegaiphp\BlogApi\Rewards;
class RewardsService
{
public string $provider = 'partner-plus';
}
Even this small example shows the real value: the CLI resolves names, paths, namespaces, and inputs for you, so the command generates something that already fits the project structure.
How to read the manifest
If you have never built a schematic before, this is the part that matters most.
name: the command name afterassegai galiases: shorter names such aslpdescription: help text shown in the CLIrequiresWorkspace: whether the schematic only works inside an Assegai appkind:declarativefor template-only generation,classfor PHP-backed generationarguments: positional inputs such as the feature nameoptions: named flags such as--provider=...templates: for declarative schematics, the files to copy and where to write themhandler: for class-backed schematics, the PHP class that performs the generation
The most important pair is:
source: where the template file lives inside the schematic foldertarget: where the generated file should be written in the app
Once those two are clear, the rest usually becomes straightforward.
How template tokens work
Tokens are simple text placeholders.
When the schematic runs, Assegai replaces them with resolved values.
Built-in naming tokens include:
__NAME____SINGULAR____PLURAL____CAMEL____KEBAB____PASCAL____BASE_NAMESPACE____CURRENT_NAMESPACE____SOURCE_ROOT__
You also get tokens for custom inputs:
__ARG_<NAME>____OPTION_<NAME>__
Examples:
__ARG_NAME____OPTION_PROVIDER__
These tokens are what make schematics practical instead of rigid. They let one command produce output that matches the app's naming conventions and folder structure automatically.
Can you combine tokens?
Yes.
Token replacement is text-based, so you can combine tokens anywhere they make sense.
Examples:
__SOURCE_ROOT__/__NAME__/DTOs/Create__SINGULAR__DTO.php
__SOURCE_ROOT__/__NAME__/__PASCAL____OPTION_PROVIDER__Client.php
docs/__KEBAB__-integration.md
If the user runs:
assegai g loyalty-program rewards --provider=PartnerPlus
then a target like this:
__SOURCE_ROOT__/__NAME__/__PASCAL____OPTION_PROVIDER__Client.php
becomes:
src/Rewards/RewardsPartnerPlusClient.php
This is where custom schematics start feeling powerful: they do not just copy files, they resolve the repeated naming work that teams usually do by hand.
Can you generate files that are not PHP?
Yes.
A schematic is not limited to PHP files. It can generate any text file.
For example:
{
"templates": [
{
"source": "templates/client.ts.stub",
"target": "__SOURCE_ROOT__/__NAME__/clients/__KEBAB__.client.ts"
},
{
"source": "templates/config.json.stub",
"target": "config/__KEBAB__.json"
},
{
"source": "templates/notes.md.stub",
"target": "docs/__KEBAB__.md"
}
]
}
That means one schematic can generate PHP, TypeScript, JSON, Markdown, YAML, or plain text files together.
This is especially useful when your real workflow spans more than backend code. A single command can generate:
- backend service classes
- DTOs
- frontend support files
- config files
- internal docs
- integration notes
That is a much better fit for real teams than a one-file generator.
When to move to a class-backed schematic
Use a class-backed schematic when the generator needs logic, not just templates.
Typical reasons:
- some files should only be generated when an option is present
- the output path depends on custom rules
- you want to assemble content in PHP before writing the file
- you want more control than declarative templates provide
Starter command:
assegai schematic:init menu-sync --php
That creates a manifest plus a handler class that extends AbstractCustomSchematic.
A good rule of thumb is:
- start declarative when your output is mostly template-driven
- move to class-backed when your generation rules start branching
Share schematics through Composer packages
If your team uses the same schematic across projects, you can ship it in a package.
In the package composer.json:
{
"extra": {
"assegai": {
"schematics": [
"resources/menu-sync/schematic.json"
]
}
}
}
Once the package is installed in the workspace, assegai g can discover it.
This makes custom schematics useful beyond one repository. They can become part of your team's internal platform, shared standards, or reusable package ecosystem.
See what the CLI found
Use:
assegai schematic:list
This shows:
- built-in schematics
- local schematics from
schematics/ - package schematics from installed Composer packages
It is the quickest way to debug discovery issues.
Discovery config
The workspace config lives in assegai.json:
{
"cli": {
"schematics": {
"paths": ["schematics"],
"discoverPackages": true,
"allowOverrides": false
}
}
}
This gives you explicit control over where custom schematics come from and whether package discovery is enabled in the current workspace.
Practical takeaway
Custom schematics are one of the strongest ways to extend Assegai without extending the framework itself.
Use them when your team wants to:
- encode repeatable feature patterns
- keep naming consistent
- scaffold more than one file at a time
- reduce copy-paste setup work
- make internal conventions executable
In other words: custom schematics let you turn your preferred architecture into a command.
Next step
If you want the full token reference, more complete manifest examples, and guidance for generating multi-file company scaffolds, continue with Custom CLI Schematics In Depth.