Skip to main content
Version: 8.x

MAML Recipes

MAML is a JSON superset with comments, raw multiline strings, optional commas, unquoted keys, and ordered objects. Files use the .maml extension.

Recipes are validated on load against MamlRecipe::schema(). 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 with dep init and pick maml when prompted.

MAML syntax in 60 seconds

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

  • Comments: # to end of line.
  • Strings: "..." with standard escapes (\t, \n, \r, \", \\, \u{XXXX}).
  • Raw strings: """...""", no escapes, newlines preserved. Use for embedded scripts.
  • Numbers: integers (5, -3) and floats (1.5, 1e9).
  • Booleans / null: lowercase true, false, null.
  • Arrays: [ ... ], comma- or newline-separated.
  • Objects: { key: value }, comma- or newline-separated. Keys are unquoted identifiers (letters, digits, _, -) or quoted strings. Quote keys with dots ("example.com") and colons ("deploy:failed").

Trailing commas allowed. Duplicate keys 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 files are required; .maml and .yaml files are parsed and applied. Use imports to bring custom PHP tasks, callbacks, or helpers into a MAML recipe.

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

Built-in recipe/* and contrib/* paths resolve via PHP's include path — no need for __DIR__ or absolute paths. See import().

config

Each key calls set($key, $value). Values can be any MAML type — string, number, bool, array, or nested object.

{
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. For runtime-evaluated values, import a .php recipe and set() from there.

hosts

Each entry calls host(). Quote keys with dots. Every nested key/value is forwarded to Host::set(), so all standard host options work: 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" }
}
}
}

Labels

Labels are key-value tags used by selectors. Define them as a nested object under labels:

{
hosts: {
"web.example.com": {
remote_user: "deployer"
labels: {
type: "web"
env: "prod"
}
}
"db.example.com": {
remote_user: "deployer"
labels: {
type: "db"
env: "prod"
}
}
}
}

Run a task on every prod host:

$ dep deploy env=prod

labels.<key> and a top-level config key with the same name (e.g. env) are independent — the selector only looks at labels.

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 either:

  1. Group task — array of strings. Runs the listed tasks in order.
  2. Step task — array of step objects. Each step is a single action or one task-config key.

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 task-config key (desc, once, hidden, limit, select). Steps run in declaration order. Config-only steps adjust task metadata and do not break the action chain.

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

Task description from comments

# comments directly above a task key become its description (joined with newlines). A 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

Use these step keys to control task metadata. They mirror the chained methods in Tasks.

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

Each action mirrors the PHP function it is named after.

cd

Change the working directory for subsequent run steps in the same task. See cd().

{ cd: "{{release_path}}" }

run

Run a command on the remote host. See run().

{
run: "php artisan migrate --force"
cwd: "{{release_path}}"
env: { APP_ENV: "production" }
secrets: { DB_PASSWORD: "s3cret" }
timeout: 600
idleTimeout: 120
nothrow: false
forceOutput: true
}
KeyTypeDefault
cwdstring{{working_path}}
cdstringalias of cwd
envobjectnone
secretsobjectnone
timeoutseconds300
idleTimeoutsecondsnone
nothrowboolfalse
forceOutputboolfalse

Multiline commands work nicely with raw strings:

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

runLocally

Run a command on the local machine. See runLocally(). Same options as run plus shell, minus cd (use cwd).

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

upload

Send files to the host. See upload(). src may be a string or array.

{
upload: {
src: "build/"
dest: "{{release_path}}/public/"
}
}

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

download

Pull files from the host. See download().

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

before, after, fail

Attach hooks to tasks. The value is a task name or an array of names. Quote names with :.

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

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

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

Arrays attach in declaration order.

Mixing MAML, PHP, and YAML

MAML covers declarative parts: config, hosts, step tasks, hooks. Anything that needs runtime PHP — closures, set('var', fn () => ...), custom step types, conditional logic — belongs in a .php recipe and gets imported both ways.

From PHP, import a MAML recipe:

import('deploy.maml');

From MAML, list the PHP file under import:

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

YAML works the same — see YAML.

Validation errors

A recipe that violates the schema raises a SchemaException pointing at the offending span. Common causes:

  • Unknown top-level key (only the keys in the table above are valid).
  • A step object with more than one action key.
  • Wrong type — e.g. config: "string" instead of an object, or tasks: [...] instead of an object.
  • Hook target that is not a string or array of strings.

Tooling

  • Editor support for VS Code, IntelliJ, Vim, and CodeMirror is listed at maml.dev.