run_command
Execute one-off commands like migrations or seeds inside a deployed app's throwaway container without interrupting the live service; reuse an idempotency key when retrying.
Instructions
Run a one-off command in a deployed service's image (migrations, seeds).
The command runs in a throwaway container built from the service's
current image with the same env, secrets, AI/DB bindings and named
volumes as the live container — the service itself keeps serving.
**Bounded-synchronous**: this call BLOCKS until the command exits or
``timeout_s`` seconds elapse, then returns ``{exit_code, timed_out,
oom_killed, duration_s, run_id, log_tail, ...}``. Branch on
``exit_code``/``timed_out`` — a non-zero exit is a normal 200 result,
not an error. One run at a time per service (409
``service.run_in_progress``).
Because the call blocks for minutes, **supply your own
``idempotency_key`` and REUSE it verbatim if you retry** — that is the
only thing standing between a client-side read timeout and a migration
running twice. A retried key REPLAYS an ``idempotent_replay`` envelope,
NOT the command's output — the run's stdout is deliberately never
cached at rest. That replay is an HTTP 200 carrying ``code:
"idempotent_replay"`` and NO ``exit_code``, so check for that code
before reading ``exit_code``. Re-run under a fresh key if you need the
output again, or read it from ``diagnose_service``'s ``last_run``.
**Key reuse protects a COMPLETED run only.** A failed call un-pins the
key, so a same-key retry really re-executes. In particular ``503
run.interrupted`` means the container started and was lost — the
command may have partially applied — and retrying it, with any key,
runs it again: stop and inspect instead. And if the daemon died
mid-run, the key stays claimed for 24 h and the retry returns ``409
idempotency_in_progress``; mint a fresh key once you have confirmed
the run is over.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| env | No | Env overrides for this run only (≤ 64 keys); omit for none. They beat the app's config env and its secrets, but lose to platform-computed keys (resolved AI/DB binding vars, ``PORT``, ``NERDIT_RUN_ID``). Put credentials HERE: values are masked in audit, scrubbed from the returned tail and never echoed back. | |
| name | Yes | Name (or row id) of a deployed APP. A model or database is refused with 422 ``run.not_supported`` — their commands and credentials are platform-managed. | |
| command | Yes | Argv, run unshelled — no globbing, no pipes (wrap in ``["sh", "-c", "…"]`` for those). It REPLACES the image's CMD and is APPENDED to its ENTRYPOINT when one is declared. 1-256 arguments, none empty or NUL-bearing. Never put credentials here: argv is host-visible via ``docker inspect`` and stored verbatim in ``last_run``. | |
| log_tail | No | Lines of scrubbed output returned. Default 200; clamped to [1, 200] here and again server-side. | |
| timeout_s | No | Seconds before the container is killed and ``timed_out: true`` returned. Default 300; floored at 1 and otherwise passed straight through, so a value above ``[services].run_timeout_max_s`` gets the authoritative 422 ``run.timeout_too_large`` instead of being silently truncated. | |
| idempotency_key | No | Caller-chosen key; omit and a fresh one is minted per call. A retry with the same key replays the first result ONLY if that run completed (2xx). Any other outcome — including 503 ``run.interrupted``, where the container started and its exit is unknown — releases the key, so the retry runs the command AGAIN: check ``last_run`` and the app before retrying a migration. |