Skip to main content
Version: 8.x

MAML Recipes

Deployer supports recipes written in MAML, a minimal, human-readable, machine-parsable configuration format. MAML extends JSON with comments, multiline raw strings, optional commas, unquoted keys, and ordered objects, while remaining strict about types and structure. Files use the .maml extension.

The schema for a MAML recipe is declared in PHP at MamlRecipe::schema() and validated on load. Validation errors point at the offending span with a source snippet.

Quick example

{
# Import other recipes (php, maml, or yaml).
import: [
"recipe/common.php"
]

config: {
repository: "git@github.com:example/example.com.git"
}

hosts: {
"example.com": {
remote_user: "deployer"
deploy_path: "~/example"
}
}

tasks: {
# Build the project
build: [
{ cd: "{{release_path}}" }
{ run: "npm ci" }
{ run: "npm run build" }
]
}

after: {
"deploy:failed": "deploy:unlock"
}
}

Generate a starter recipe interactively with:

dep init

and choose maml when prompted for the recipe language.

MAML syntax in 60 seconds

A MAML document is a single value, normally a top-level object { ... }.

  • Comments: # to end of line.
  • Strings: double-quoted ("..."), with the usual escapes (\t, \n, \r, \", \\, \u{XXXX}).
  • Raw strings: triple-quoted ("""..."""), no escapes, newlines and whitespace preserved verbatim. Useful for embedding scripts.
  • Numbers: integers (5, -3) and floats (1.5, 1e9).
  • Booleans / null: true, false, null (lowercase only).
  • Arrays: [ ... ], comma- or newline-separated.
  • Objects: { key: value }, comma- or newline-separated. Keys may be unquoted identifiers (letters, digits, _, -) or quoted strings. Hosts with dots ("example.com") and hook names ("deploy:failed") must be quoted.

Trailing commas are allowed everywhere. Duplicate keys inside an object are not.

Top-level sections

A recipe is an object with these optional keys, validated by the schema:

KeyDescription
importString or array of strings. Paths to other recipes (.php, .maml, .yaml).
configObject. Becomes calls to set().
hostsObject. Each entry becomes host() (or localhost() when local: true).
tasksObject. Each entry becomes a task().
beforeObject mapping task → hook(s). Becomes before().
afterObject mapping task → hook(s). Becomes after().
failObject mapping task → fallback task. Becomes fail().

Any other top-level key is rejected with a schema error.

import

Pull in other recipes. PHP recipes run as plain require, MAML and YAML recipes are parsed and applied. This is how a MAML recipe gains access to custom PHP tasks, callbacks, and helpers it cannot express directly.

{
import: "recipe/laravel.php"
}
{
import: [
"recipe/common.php"
"deploy/custom.php"
"deploy/extras.maml"
]
}

config

A flat object. Each key is forwarded to set($key, $value). Values may be strings, numbers, booleans, arrays, or nested objects, anything MAML can express.

{
config: {
repository: "git@github.com:example/example.com.git"
keep_releases: 5
ssh_multiplexing: true
shared_dirs: ["storage", "bootstrap/cache"]
}
}

config does not accept PHP closures. To set values that need runtime evaluation, import a .php recipe and call set() from there.

hosts

Each entry creates a host. Keys with dots (example.com) must be quoted. Inside, every key/value is forwarded to Host::set(), so all standard host options are available (remote_user, deploy_path, port, identity_file, labels, ssh_arguments, etc.).

{
hosts: {
"prod.example.com": {
remote_user: "deployer"
deploy_path: "/var/www/prod"
labels: { stage: "production" }
}
"staging.example.com": {
remote_user: "deployer"
deploy_path: "/var/www/staging"
labels: { stage: "staging" }
}
}
}

Localhost

Set local: true to register the entry as a localhost via localhost():

{
hosts: {
"dev": {
local: true
deploy_path: "/tmp/dev"
}
}
}

tasks

A task entry is one of:

  1. Group task: an array of strings. Runs the listed tasks in order.
  2. Step task: an array of step objects. Each step is a single action.

Group tasks

{
tasks: {
deploy: [
"deploy:prepare"
"deploy:vendors"
"deploy:publish"
]
}
}

Step tasks

Each step is an object with exactly one action key (cd, run, runLocally, upload, download) or one or more task-config keys (desc, once, hidden, limit, select). Steps are executed in declaration order. Task-config steps modify the task itself and do not interrupt the chain of actions.

{
tasks: {
build: [
{ desc: "Build assets" }
{ once: true }
{ cd: "{{release_path}}" }
{ run: "npm ci" }
{ run: "npm run build" }
]
}
}

Task description from comments

Leading # comments directly above a task key become the task's description (joined with newlines). The desc step takes precedence if both are present.

{
tasks: {
# Deploy the application
# Runs migrations, builds assets, restarts services
deploy: [
{ run: "echo deploying" }
]
}
}

Task config keys

Set these inside step objects to control task metadata:

KeyTypeEffect
descstringSets the description (shown in dep list).
onceboolRun on a single host only.
hiddenboolHide from dep list.
limitnumberMaximum hosts to run on in parallel.
selectstringHost selector expression (see Selector).
{
tasks: {
migrate: [
{ desc: "Run database migrations" }
{ once: true }
{ limit: 1 }
{ select: "stage=production" }
{ run: "php artisan migrate --force" }
]
}
}

Step actions

cd

Change the working directory for subsequent run steps in the same task.

{ cd: "{{release_path}}" }

run

Execute a command on the remote host. Equivalent to run(). All optional keys mirror the PHP function:

{
run: "php artisan migrate --force"
cwd: "{{release_path}}"
env: {
APP_ENV: "production"
}
secrets: {
DB_PASSWORD: "s3cret"
}
timeout: 600
idleTimeout: 120
nothrow: false
forceOutput: true
}
OptionTypeDefault
cwdstringhost's cwd/deploy_path
cdstring(alias of cwd)
envmap<string, string>none
secretsmap<string, string>none
timeoutnumber (seconds)300
idleTimeoutnumber (seconds)none
nothrowboolfalse
forceOutputboolfalse

Use a raw string for multiline commands:

{
run: """
set -e
php artisan down
php artisan migrate --force
php artisan up
"""
}

runLocally

Run a command on the local machine. Mirrors runLocally().

{
runLocally: "git rev-parse HEAD"
cwd: "."
shell: "/bin/bash"
timeout: 60
}

Supports the same options as run plus shell, except cd (use cwd).

upload

Transfer files to the remote host. Mirrors upload(). src may be a single path or an array of paths.

{
upload: {
src: "build/"
dest: "{{release_path}}/public/"
}
}
{
upload: {
src: ["dist/app.js", "dist/app.css"]
dest: "{{release_path}}/public/assets/"
}
}

download

Transfer files from the remote host to the local machine. Mirrors download().

{
download: {
src: "{{deploy_path}}/shared/.env"
dest: ".env.production"
}
}

before, after, fail

Hooks attach tasks to other tasks. The value may be a single task name or an array of task names. Quote names that contain :.

{
before: {
deploy: ["deploy:prepare", "build"]
}

after: {
"deploy:failed": "deploy:unlock"
deploy: "deploy:cleanup"
}

fail: {
deploy: "deploy:rollback"
}
}

For arrays, hooks attach in declaration order.

Mixing MAML, PHP, and YAML

MAML covers the declarative parts of a recipe: config, hosts, tasks built from standard steps, hooks. Anything that needs runtime PHP (closures, the set('var', fn () => ...) pattern, custom step types, conditional logic) belongs in a .php recipe imported from MAML, or vice-versa.

From a PHP recipe, import MAML using import():

import('deploy.maml');

From a MAML recipe, list the PHP file under import:

{
import: ["deploy/extras.php"]
}

The same applies to YAML, see YAML.

Validation errors

When a recipe does not match the schema, Deployer raises a SchemaException with the offending span and a snippet of source. Common causes:

  • Unknown top-level key (anything outside the table above).
  • A step object with multiple action keys (each step is one action).
  • Wrong types, e.g. config: "string" instead of an object, or tasks: [...] instead of an object.
  • Hook target not declared as a string or array of strings.

Fix the structure, re-run, and the error trace will pinpoint the line.

Output and tooling

  • dep init generates a starter deploy.maml.
  • dep config prints config in MAML by default; use --format=json or --format=yaml for other formats.
  • Editor support for MAML is available for VS Code, IntelliJ, Vim, and CodeMirror, see maml.dev.