Recipes and config actions
A recipe is a directory with a recipe.yml file that installs extensions, imports configuration, applies configuration actions, and creates default content, all in one apply step. This page covers the structure of recipe.yml, the configuration actions that let a recipe modify config it does not own, how to apply a recipe, and how to choose between a recipe, a module, and a config sync.
Package a set of site changes (modules to install, config to import, permissions to grant) into a recipe that you apply to an existing site. Reach for a recipe when you want a repeatable, composable building block that configures a site but does not ship runtime PHP. If you need to ship code (a service, a plugin, an entity type), that belongs in a module; see the decision table below.
Prerequisites
Section titled “Prerequisites”- A Drupal 11 site you can run CLI commands against.
- Familiarity with how Drupal stores configuration. See Config, content, state, and sessions.
- A reading knowledge of YAML.
A recipe is a directory
Section titled “A recipe is a directory”A recipe lives in its own directory with a recipe.yml file at the root. The directory may also contain a config/ directory (config to import on apply) and a content/ directory (default content entities). Core ships more than 20 recipes under core/recipes/; the Standard recipe is what builds a standard install.
The recipe.yml file has a small set of top-level keys. The Recipe class parses and validates them on apply:
| Key | Required | Description |
|---|---|---|
name | yes | Human-readable name shown while applying. |
description | no | One short, single-line sentence. |
type | no | A grouping label, similar to package in a *.info.yml file. |
recipes | no | A list of other recipes to apply first. This is how recipes compose. |
install | no | A list of module and theme machine names to install. Core detects which is which. |
config | no | What configuration to import and which config actions to run. |
input | no | Values collected at apply time (a prompt or a form), substituted into actions. |
content | no | Default content, created after config is installed. |
Declare the name, type, and what to install
Section titled “Declare the name, type, and what to install”Start with the identifying keys and the install list. Every module or theme named in install is installed if it is not already. During this install, core creates only simple configuration from those extensions, so the recipe keeps control over the configuration entities:
name: 'Event starter'description: 'Installs the content modules and grants anonymous users content access.'type: 'Content type'install: - node - text - datetimeImport configuration from the installed extensions
Section titled “Import configuration from the installed extensions”The config.import key lists configuration to import from the extensions the recipe installs. For each extension you list either '*' (import all of that extension’s shipped config, plus optional config whose dependencies are now met) or an explicit list of config names:
config: import: node: '*' text: - text.settingsImporting config this way means a recipe can rely on configuration entities provided by a module without copying those YAML files into its own config/ directory.
Run config actions
Section titled “Run config actions”Config actions modify configuration the recipe does not necessarily own. They sit under config.actions, keyed by the configuration name, then by the action ID, then the data for that action. Here is a complete recipe that installs modules, imports config, and runs two actions:
name: 'Event starter'description: 'Installs the content modules and grants anonymous users content access.'type: 'Content type'install: - node - text - datetimeconfig: import: # Import the text format config the node body field depends on. node: '*' actions: # Grant the anonymous role permission to view published content. user.role.anonymous: grantPermission: 'access content' # Update simple (non-entity) config with a list of key/value pairs. system.site: simpleConfigUpdate: page.front: '/node'The next section explains what those actions do.
Config actions
Section titled “Config actions”A config action is a plugin that manipulates one config object. Core resolves a short action ID (such as grantPermission) against the config entity type of the target config name, then runs the matching plugin. The plugin manager is \Drupal\Core\Config\Action\ConfigActionManager, and the topic is the Config Action API.
There are two families of action.
Simple config updates
Section titled “Simple config updates”For simple configuration (settings that are not config entities), the simpleConfigUpdate action takes a map of keys to new values and writes them:
system.site: simpleConfigUpdate: page.front: '/node'The data must be an array of key/value pairs. The SimpleConfigUpdate plugin is for simple config only. Using it on a config entity is deprecated in Drupal 11.2 and throws an exception from Drupal 12, because entities have their own methods for changes. Use setProperties for a config entity instead.
Entity-method actions
Section titled “Entity-method actions”Config entities expose actions through their own methods. A method tagged with the ActionMethod attribute becomes a config action whose ID is the method name. The user Role entity tags grantPermission(), so a recipe can grant a permission to a role by name:
user.role.anonymous: grantPermission: 'access content'Because the attribute pluralizes method names by default, grantPermission also gains a grantPermissions form that accepts a list and calls the method once per item:
user.role.authenticated: grantPermissions: - 'access content' - 'delete own files'When the data is not an array, core maps it to the method’s single argument. When it is an array and the method takes more than one parameter, core unpacks it into the arguments. This mapping is implemented in the EntityMethod plugin.
Two more entity actions are worth knowing:
createcreates a config entity and fails if one already exists;createIfNotExistscreates it only when it is absent and returns early otherwise. Both come fromthe EntityCreate plugin, which derives the two action IDs.- A config name can target many entities with a wildcard.
core.entity_view_display.node.*.defaultmatches the default view display of every content type. The prefix before the first wildcard must be a known config entity type prefix.
Optional actions
Section titled “Optional actions”Prefix a config name with ? to make its actions optional. If the target config does not exist, core skips the actions instead of failing. This is how a recipe can configure a module that may or may not be present:
# Runs only if an editor role already exists.?user.role.editor: grantPermission: 'access content overview'Apply the recipe
Section titled “Apply the recipe”Recipes apply through the core command-line tool, dr. The command is recipe:apply (with recipe kept as an alias), and it takes the path to the recipe directory:
ddev exec vendor/bin/dr recipe:apply web/modules/custom/docs_snippets/recipes/automate_event_starterThe RecipeRunner processes a recipe in a fixed order: apply listed sub-recipes first, install modules then themes, install and import configuration, run config actions, then create content. Before applying, the tool takes a configuration checkpoint so you can roll back.
The RecipeRunner applies a recipe in a fixed order regardless of how the keys are arranged in recipe.yml: listed sub-recipes first, then modules, then themes, then config install and import, then config actions, then content. Each step depends on the prior one, so the order is not the order keys appear in the file.
You can also build a brand new site from a recipe. The core installer and drush site:install both accept a recipe path in place of an install profile:
ddev drush site:install web/core/recipes/standardVerify
Section titled “Verify”After applying, confirm the changes landed:
# The modules from the install list are now enabled.ddev drush pm:list --status=enabled --type=module
# The granted permission is on the role.ddev drush config:get user.role.anonymous permissionsYou can also check the front page setting with ddev drush config:get system.site page.front, which now reports /node.
Variations
Section titled “Variations”- Compose recipes. List other recipes under the
recipeskey to apply them first. TheStandardrecipe is built almost entirely from smaller recipes (basic_block_type,administrator_role, and others), which keeps each one focused and reusable. - Collect input. The
inputkey defines values gathered at apply time, from a console prompt or a form, with a default sourced from a literalvalue, aconfigobject, or anenvvariable. Core wraps each input key as${key}and substitutes it into action names and data before the actions run. - Strict config. Under
config,strictdefaults totrue: any config the recipe ships that already exists on the site must match the recipe’s copy exactly, or the apply fails.strict: falseskips config that already exists. A list of config names makes only those strict. TheStandardrecipe is strict because it builds a site from scratch;editorial_workflowis not, because its view may already be present.
Module vs recipe vs config sync
Section titled “Module vs recipe vs config sync”These three tools overlap, so pick by what you are shipping and how often you apply it.
| Module | Recipe | Config sync | |
|---|---|---|---|
| Ships PHP (services, plugins, entity types) | yes | no | no |
| Installs extensions | itself | yes, a list | no |
| Applies to | the site, by being installed | any site, repeatedly | one site’s own config |
| Safe to re-apply | n/a | when written for it (createIfNotExists, optional targets, strict: false); a failed apply rolls back to a checkpoint | yes, by definition |
| Best for | runtime behavior and code | a composable, reusable configuration building block | moving a single site’s full config between its own environments |
Use a module when you are writing code. Use a recipe when you are packaging a configured feature that other sites (or a new build of this site) should be able to apply. Use a config sync (covered in Configuration management workflow) to move one site’s complete configuration from development to production. A recipe and a config sync are not rivals: a recipe is how you set a site up, and config sync is how you keep that one site’s environments aligned afterward.
How Drupal CMS uses recipes
Section titled “How Drupal CMS uses recipes”The largest recipe consumer is Drupal CMS: its site templates (Starter, Byte) and every add-on in its browser catalog are recipes applied through the UI. When a Drupal CMS user selects an add-on, the machinery on this page runs: extensions install, config imports, config actions fire. If you want your feature installable from that catalog, package it as a recipe.
Related
Section titled “Related”- Configuration management workflow for exporting and importing a single site’s config.
- Config, content, state, and sessions for the difference between simple config and config entities.
The Config Action API topicandthe Configuration API topicon api.drupal.org.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.