Skip to content

Exec (WP-CLI & shell)

Previewcapability-gated

The exec resource runs commands inside an environment and returns the result as a streaming job — a typed exit code, split stdout and stderr, and a live SSE tail. It’s the same WP-CLI you already run over the SSH gateway, reshaped into a first-class, scoped, idempotent API primitive that automation and agents can call safely.

the scope split — this is the important part

Section titled “the scope split — this is the important part”

There are two ways to run a command, and they are deliberately not the same scope:

Scope What it grants Isolation
wp.cli:exec Scoped WP-CLI only — wp <subcommand>, no raw shell. In every preset; the safe default for WP automation.
exec:raw Arbitrary shell — bash -c '…'. Keys-to-the-kingdom. Excluded from every wildcard and preset. Granted one key at a time, explicitly.

A * wildcard key can run WP-CLI but cannot run raw shell. exec:raw is the single most dangerous scope on the platform, so it never rides along with anything else — you grant it deliberately or not at all. See the security model for why dangerous scopes are isolated.

POST …/{envID}/exec   wp.cli:exec or exec:raw

Returns 202 Accepted with a job and a Location header. Send an Idempotency-Key so a retried command never runs twice.

Parameter Type Required Description
shell string yes wp-cli (needs wp.cli:exec) or bash (needs exec:raw).
command array yes The command and its arguments, e.g. ["plugin", "list", "--status=active"].
timeout integer no Max seconds before the job is killed. Example default 30.
cwd string no Working directory for bash shells. Ignored for wp-cli.

The job’s result carries the typed outcome:

{
"exit_code": 0,
"stdout": "woocommerce\nwordpress-seo\n",
"stderr": "",
"duration_ms": 842
}

A non-zero exit_code produces a succeeded job whose result.exit_code is non-zero — the job ran, the command failed. Distinguish that from a failed job, which means the platform couldn’t run the command at all.

GET · POST …/{envID}/exec/bookmarks  

wp.cli:exec

Save a frequently-run command under a name and re-run it by reference, so a remediation runbook lives on the platform rather than scattered across shell history.

POST a bookmark
{ "name": "flush-and-warm", "shell": "wp-cli",
"command": ["cache", "flush"] }

Run a saved bookmark with POST …/exec { "bookmark": "flush-and-warm" }.

POST …/{envID}/exec:batch  

wp.cli:exec

Run an ordered list of commands in one call — the typed equivalent of a small script. Returns one job; result reports each step’s exit code and output.

Request body
{
"commands": [
{ "shell": "wp-cli", "command": ["plugin", "update", "--all"] },
{ "shell": "wp-cli", "command": ["cache", "flush"] }
],
"stop_on_error": true
}

Update all plugins on staging and watch the output stream live.

  1. Run the command. A wp.cli:exec-scoped key, an idempotency key, a 202 back.

    wp plugin update --all on staging
    curl -X POST https://api.managed.dev/v1/sites/site_01J7.../environments/env_01J8.../exec \
    -H "Authorization: Bearer mfk_live_9aF2..." \
    -H "Forge-Version: 2026-06-23" \
    -H "Idempotency-Key: a91f-22b7-01de" \
    -H "Content-Type: application/json" \
    -d '{ "shell": "wp-cli", "command": ["plugin", "update", "--all"] }'
    202 response
    HTTP/1.1 202 Accepted
    Location: /v1/jobs/job_01J9...
    {
    "data": {
    "id": "job_01J9...", "type": "exec.run", "status": "queued",
    "created_at": "2026-06-23T18:10:02.004Z",
    "resource": { "type": "exec", "shell": "wp-cli",
    "site_id": "site_01J7...", "env_id": "env_01J8..." }
    },
    "request_id": "req_01J9..."
    }
  2. Tail the output. SSE streams stdout/stderr chunks as they arrive.

    Live-tail the exec job
    curl -N https://api.managed.dev/v1/jobs/job_01J9.../stream \
    -H "Authorization: Bearer mfk_live_9aF2..."
    job_01J9… — exec stream
    event: stdout
    Plugin ‘woocommerce’ updated to 9.1.2
    Plugin ‘wordpress-seo’ updated to 23.5
    event: done
    ✓ exit_code 0 · 2 updated · 1.9s
  3. Read the typed result. When the job reaches succeeded, inspect result.exit_code to gate the next step. See async jobs.