Skip to content

How-to guideFor module developers, site buildersDrupal 11

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.

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:

KeyRequiredDescription
nameyesHuman-readable name shown while applying.
descriptionnoOne short, single-line sentence.
typenoA grouping label, similar to package in a *.info.yml file.
recipesnoA list of other recipes to apply first. This is how recipes compose.
installnoA list of module and theme machine names to install. Core detects which is which.
confignoWhat configuration to import and which config actions to run.
inputnoValues collected at apply time (a prompt or a form), substituted into actions.
contentnoDefault 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
- datetime

Import 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.settings

Importing 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.

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:

recipe.yml
name: 'Event starter'
description: 'Installs the content modules and grants anonymous users content access.'
type: 'Content type'
install:
- node
- text
- datetime
config:
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.

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.

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.

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:

  • create creates a config entity and fails if one already exists; createIfNotExists creates it only when it is absent and returns early otherwise. Both come from the EntityCreate plugin, which derives the two action IDs.
  • A config name can target many entities with a wildcard. core.entity_view_display.node.*.default matches the default view display of every content type. The prefix before the first wildcard must be a known config entity type prefix.

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'

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:

Terminal window
ddev exec vendor/bin/dr recipe:apply web/modules/custom/docs_snippets/recipes/automate_event_starter

The 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.

Recipe apply order

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:

Terminal window
ddev drush site:install web/core/recipes/standard

After applying, confirm the changes landed:

Terminal window
# 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 permissions

You can also check the front page setting with ddev drush config:get system.site page.front, which now reports /node.

  • Compose recipes. List other recipes under the recipes key to apply them first. The Standard recipe is built almost entirely from smaller recipes (basic_block_type, administrator_role, and others), which keeps each one focused and reusable.
  • Collect input. The input key defines values gathered at apply time, from a console prompt or a form, with a default sourced from a literal value, a config object, or an env variable. Core wraps each input key as ${key} and substitutes it into action names and data before the actions run.
  • Strict config. Under config, strict defaults to true: any config the recipe ships that already exists on the site must match the recipe’s copy exactly, or the apply fails. strict: false skips config that already exists. A list of config names makes only those strict. The Standard recipe is strict because it builds a site from scratch; editorial_workflow is not, because its view may already be present.

These three tools overlap, so pick by what you are shipping and how often you apply it.

ModuleRecipeConfig sync
Ships PHP (services, plugins, entity types)yesnono
Installs extensionsitselfyes, a listno
Applies tothe site, by being installedany site, repeatedlyone site’s own config
Safe to re-applyn/awhen written for it (createIfNotExists, optional targets, strict: false); a failed apply rolls back to a checkpointyes, by definition
Best forruntime behavior and codea composable, reusable configuration building blockmoving 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.

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.

Was this page helpful?

Drupal is a registered trademark of Dries Buytaert.