platform.yml commands:
The per-service commands: block declares named commands the platform can run on demand inside that service’s running container. There is no arbitrary-command surface — only catalog entries run.
This page is the parse shape: the keys and constraints parsePlatformConfig actually accepts. Usage (portal Commands tab, MCP, schedules, run history) lives in Writing platform.yml. WordPress’s wp:* presets live in WordPress on Tandem.
commands: is valid on any service type (build-from-source and image alike). It is snapshotted onto the deployment at deploy time, so the catalog matches the code in that container.
It is not any of these neighboring fields:
| Field | What it is |
|---|---|
Service-level command: |
image services only: override the image’s baked CMD (string or argv). |
Service-level jobs: |
image and dockerfile services: one-off pre/post lifecycle jobs, once per deploy, in the new image (docker run --rm). See Deploy jobs. |
build: / start: / install: |
Shell strings. Command run is never a shell string. |
The block is a map of command name → command object. Unknown keys on a command object are rejected.
services:
api:
path: api
type: node
start: npm start
commands:
migrate:
run: ["npm", "run", "migrate"]
description: Apply database migrations
role: admin
timeoutSeconds: 120
recover:
run: ["node", "dist/cli/recover.js"]
params:
- name: phase
label: Recovery phase
- name: mode
label: Recovery mode
Command names
Map keys must match ^[a-z0-9][a-z0-9:_-]{0,63}$ — start with a lowercase letter or digit, then up to 63 more of a-z, 0-9, :, _, or - (64 characters total). Colons are allowed (wp-audio:recover, wp:login). Spaces and uppercase letters are not.
Command object
Valid keys: run, description, role, timeoutSeconds, params, enabled. The object is strict.
| Field | Required | Default | Shape |
|---|---|---|---|
run |
yes | — | Argv array of one or more non-empty strings. Exec form only — never a shell string. |
description |
no | — | String, max 500 characters. Shown in the portal and MCP catalog. |
role |
no | developer |
Minimum org role: developer, admin, or owner. |
timeoutSeconds |
no | 60 | Integer, 1–300. The command is killed at this deadline. |
params |
no | — | Array of at most 8 param objects (see below). |
enabled |
no | true |
false hides this name from the catalog (the preset-opt-out). |
There is no timeout key — the error names timeoutSeconds as the closest match. There is no schedule, injectActorEmail, shell, or command key on a command object.
run is never interpolated and never passed through a shell. No pipes, globs, or &&. Put anything complex in a script and list that script in run.
Param objects
Each params entry is an object, not a bare string. Valid keys: name, label. The object is strict.
| Field | Required | Shape |
|---|---|---|
name |
yes | ^[a-z][a-z0-9_-]{0,31}$ — starts with a letter, then up to 31 more of a-z, 0-9, _, or - (32 characters total). No colon. |
label |
no | String, max 100 characters. Display-only. |
There is no required, description, type, or default on a param. Every declared param is required at run time.
At execution, values are appended to run in declared order as discrete argv items. Object-key order in the call does not change argv order. Undeclared keys are rejected. Each value is a non-empty string, at most 500 characters, and must not contain NUL.
# Wrong — rejected: expected { name: string, label?: string }, received string
params: [phase, mode]
# Right
params:
- name: phase
- name: mode
Merge with platform presets
Two sources feed one catalog:
- Platform presets attached by detection (today: WordPress, when deploy records
wordpress=true). - Repo-declared
commands:.
Repo wins by name. Declaring the same name as a preset replaces it. enabled: false removes that name (including a preset). Repos cannot declare injectActorEmail — that flag exists only on platform presets (for example wp:login) and is dropped if you override that name.
WordPress preset names in source today: wp:login, wp:cache-flush, wp:plugin-list, wp:user-list, wp:plugin-install, wp:plugin-update, wp:theme-install, wp:theme-update, wp:core-update, wp:search-replace.
What a rejected file looks like
A bad commands: block fails the whole platform.yml, so every service in the project stays blocked. Previously deployed containers keep serving. Common shapes:
run: npm run migrate— expected an argv array.timeout: 120— unknown key; valid keys arerun,description,role,timeoutSeconds,params,enabled.params: [phase, mode]— each entry must be{ name, label? }."Bad Name!":— command name fails the regex above.
The parse error names the path (services.api.commands.wp-audio:recover.params[0]) and the expected shape at that path.