A Drupal module providing standalone Symfony Console 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).
Commands run through dr, Drupal core's own native CLI, introduced in
Drupal 11.4 — Drush is not required to run them (see Development
notes).
- Drupal
^11.4— needed for the nativedrCLI and its#[AsCommand]auto-discovery; this module doesn't work on earlier core versions 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) - The module still needs to be enabled for
drto discover its commands — do that however your site normally enables modules (drush en commands, the UI,drush/drushisn't otherwise required for anything in this repo)
composer require previousnext/commands # or however this repo is required
drush en commands # or enable it via the UI| 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 dr list on your own site to confirm what's registered, and
dr help <command> for full option details.
Creates a node type and saves it to active config.
dr content-type:create research "Research" --description="Research content"
dr 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.
dr content-type:field:add research introduction --reuse
# Create a new field.
dr content-type:field:add research pub_date --type=datetime --label="Publication date"
dr 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.
dr 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}
dr 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.
dr content-type:bundle-class:generate research --module=csnsw_node --class=Research
# With an enum case:
dr 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 ad hoc PHP one-liners for.
dr content-type:list field-storages
dr content-type:list node-types
dr content-type:list roles
dr content-type:list workflows
dr content-type:list modules
dr content-type:list enums
dr content-type:list entity-types
dr 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.
dr ships with Drupal core as of 11.4 — no separate install
step, and no Drush required. It boots Drupal, scans src/Command/ in every
enabled module for #[AsCommand]-attributed classes, autowires their
constructors from the service container, and registers each one, all
without any services.yml entries.
dr content-type:create research "Research"
dr content-type:list rolesNote that generic Drupal operations that aren't part of this module —
config:export, cache:rebuild, and so on — are unaffected by any of this
and still go through Drush (or whatever else your site uses for them); dr
only knows about commands, not Drush's broader command set.
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
dr 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 living undersrc/Command/, in theDrupal\commands\Commandnamespace. That namespace/directory pair is exactly what Drupal core'sdrCLI scans for in enabled modules — noservices.ymlentry, no manualconsole.commandtag. Constructors are autowired straight from the container, same as any other Drupal service. commands.services.ymlonly declares the plain helper services below — none of the command classes themselves are listed there.- 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. - If you enable/disable the module or add a new command class, run
drush cache:rebuild(or clear caches however your site normally does) so the container is rebuilt before testing withdr.