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:

  1. Platform presets attached by detection (today: WordPress, when deploy records wordpress=true).
  2. 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 are run, 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.