A Drupal module providing standalone Drush commands for content-type
authoring — creating content types, adding fields, attaching workflows,
granting permissions, and generating bundle classes. Every command saves
directly to active config via the entity API; nothing reads or writes YAML
files directly. Run drush config:export when you're done to persist
changes to disk.
Each command works two ways:
- Interactively — run it with no arguments (or with some missing) and it prompts for whatever's needed.
- Non-interactively — pass every argument/option plus
--no-interaction, suitable for scripting or for an agent (see Using with Claude Code below).
- Drupal
^10 || ^11 drush/drush ~13— commands are Symfony Console classes (#[AsCommand]), registered explicitly viadrush.services.ymlrather than relying on Drush's classic auto-discovery (see Development notes)drupal:node(declared incommands.info.yml)- Optional, only if you use the matching command: Drupal core's Workflows
module (
content-type:workflow:add), and the contrib Scheduled Transitions module if you want the 3 scheduled-transition permissions thatcontent-type:permissions:grantotherwise skips (see below)
composer require previousnext/commands # or however this repo is required
drush en commands| Command | Alias | Purpose |
|---|---|---|
content-type:create |
ctc |
Create a new content type |
content-type:field:add |
ctfa |
Add a field to a content type |
content-type:workflow:add |
ctwa |
Attach a content type to a workflow |
content-type:permissions:grant |
ctpg |
Grant standard node permissions for a content type to roles |
content-type:bundle-class:generate |
ctbcg |
Generate a PHP bundle class (and optional enum case) for a content type |
content-type:list |
ctl |
List reference data (fields, node types, roles, workflows, modules, enums, entity types/bundles) as JSON |
Run drush list --filter=content-type on your own site to confirm what's
registered, and drush help <command> for full option details.
Creates a node type and saves it to active config.
drush content-type:create research "Research" --description="Research content"
drush content-type:create research "Research" --help-text="Shown on the node form" --no-revisionArguments: machine_name (required), label (required).
Options: --description, --help-text, --no-revision (disables new
revisions on save; revisions are on by default).
Adds a field to a content type — either reusing an existing field storage from another bundle, or creating a brand-new one. Loops to add multiple fields in one interactive run; non-interactively it adds exactly one per invocation.
# Reuse an existing field storage on another bundle.
drush content-type:field:add research introduction --reuse
# Create a new field.
drush content-type:field:add research pub_date --type=datetime --label="Publication date"
drush content-type:field:add research authors --type=entity_reference --cardinality=-1 \
--_type_settings='{"storage_settings":{"target_type":"user"},"field_settings":{"handler":"default:user","handler_settings":{"target_bundles":null}}}'Arguments: bundle, field_name (both optional — prompted for if missing).
Options: --reuse, --type (field type plugin ID, required unless
--reuse), --label, --description, --required, --cardinality
(-1 for unlimited, default 1), --_type_settings (JSON-encoded
{"storage_settings": {...}, "field_settings": {...}} — the type-specific
settings the interactive prompts would otherwise collect).
Attaches a content type to a workflow by updating the workflow config
entity directly.
drush content-type:workflow:add research csnsw_workflowArguments: bundle, workflow_id (both optional). Fails cleanly with an
"enable the Workflows module" error if the workflow entity type doesn't
exist on the site, rather than crashing.
Grants the standard set of node content permissions for a bundle to one or more roles:
create/edit any/delete any {bundle} contentdelete/revert/view {bundle} revisionsadd/reschedule/view scheduled transitions node {bundle}
drush content-type:permissions:grant research --roles=editor,content_adminArguments: bundle (optional). Options: --roles (required,
comma-separated role IDs).
The 3 scheduled-transitions permissions only exist if the Scheduled
Transitions contrib module is installed and the bundle has been
explicitly opted into it via scheduled_transitions.settings — being on a
Content Moderation workflow isn't enough on its own. The command checks
what actually exists on the site before granting, warns about anything it
skips, and reports the real number granted — it never reports success for a
permission it couldn't actually assign.
Generates a #[Bundle]-attributed PHP class for a content type (using
Drupal\bca\Attribute\Bundle from the contrib BCA module — the generated
class won't compile without it) and registers it on the node_type entity.
Optionally adds a case to an existing backed-string enum, or scaffolds a new
one.
drush content-type:bundle-class:generate research --module=csnsw_node --class=Research
# With an enum case:
drush content-type:bundle-class:generate research_paper \
--module=csnsw_node --class=ResearchPaper \
--enum='Drupal\csnsw_node\CsnswNodeTypes' --enum-case=RESEARCH_PAPERArguments: bundle (optional). Options: --module, --class, --enum
(an existing enum's FQN, new:{module}:{EnumClassName} to scaffold one, or
omit/none to skip), --enum-case (UPPER_SNAKE_CASE, required unless the
enum is skipped).
Writes {module}/src/Entity/Node/{ClassName}.php and either updates or
creates the enum file — these are real PHP source file writes, not config
entities, so review the diff before committing.
Prints JSON reference data for the other commands — the thing you'd
otherwise need drush php:eval one-liners for.
drush content-type:list field-storages
drush content-type:list node-types
drush content-type:list roles
drush content-type:list workflows
drush content-type:list modules
drush content-type:list enums
drush content-type:list entity-types
drush content-type:list bundles --entity-type=taxonomy_termArgument: subject — one of field-storages, node-types, roles,
workflows, modules, enums, entity-types, bundles. bundles
requires --entity-type. workflows returns {} if the Workflows module
isn't enabled, rather than erroring.
There's a Claude Code skill, create-content-type, that drives all of the
above end-to-end: it asks you the same questions an interactive terminal run
would (content type basics, which fields to reuse, new fields with
type-specific settings, workflow, permissions, optional bundle class), using
content-type:list to fetch live site data instead of guessing, and runs
every command non-interactively once it has your answers. Say something like
"create a new content type", "add fields to a content type", or
"scaffold a content type" to trigger it, or invoke it directly as
/create-content-type.
The skill lives outside this repo (in a shared skills directory) and expects this module to be enabled and Docker Compose running — see the skill file itself for the full prerequisites and step-by-step workflow.
opencode/tools/content-type.ts wraps create, field_add, workflow_add,
permissions_grant, and config_export as typed OpenCode tools (it doesn't
yet cover content-type:bundle-class:generate or content-type:list — use
drush directly for those). Symlink or copy it to
.opencode/tools/content-type.ts in your project to pick it up.
- Every command is a plain Symfony Console
#[AsCommand]class, not aDrushCommands/Robo-style command — except nothing here currently uses that style. This matters because Drush's classic auto-discovery (CommandFileDiscovery) only picks upDrushCommandssubclasses; SymfonyCommandclasses must be explicitly registered asconsole.command-tagged services indrush.services.yml, or Drush never sees them at all. drush.services.ymldoes not support_defaults: autowire. Drush'sLegacyServiceInstantiatorskips the entire file if it seesautowire: trueanywhere in it — not just that one service, the whole file, silently (only aninfo-level log line explains why). Every argument indrush.services.ymlmust be listed explicitly.- All
console.command-tagged services are eagerly instantiated at Drush boot, for every command, on every invocation — not lazily, and not just for the command being run. A broken constructor (wrong type-hint, missing service) in any one command's class takes down everycontent-type:*command until it's fixed. Rundrush cache:rebuildafter changing this module's services or commands, and test withdrush list --filter=content-typebefore assuming a single command's fix is isolated. - Shared logic lives in
src/Service/:FieldStorageReader(existing node fields/types),FieldTypeSettingsCollector(interactive field-type settings),ModuleDiscoveryService(custom module discovery),FileGeneratorService(Twig rendering via Drupal Code Generator, backingbundle-class:generate), andContentTypeDiscoveryService(backscontent-type:list).src/ConsoleCommandTrait.phpandsrc/Trait/EnumDiscoveryTrait.phphold small helpers shared across commands.