لوكلاء AI: ملف markdown لهذه الصفحة — /docs-content-en/infra/deploy.md فهرس التوثيق — /llms.txt
مقالات التوثيق متاحة حاليًا باللغة الإنجليزية فقط.
Deploy API
If GET /v1/me includes deployment.standalone.reuseTarget, use its serverId: inspect the server first, then deploy to that same id. Creating a new VM is unnecessary. Without that signal, check capabilities.servers.create.available first. For TRIAL_PORTAL_LIMIT, do not cycle through names, keys, providers or plans: follow userMessage and alternatives. Successful create-reuse returns HTTP 201 with reused: true; APPLICATION_REUSE_CONFLICT (HTTP 409, retryable: true) means a concurrent binding change — retry the request.
Deploying applications to BLACKHOLE servers without SSH. All operations go through the tunnel agent: commands run on the server, files are uploaded through the tunnel, logs are read from the system journal (the journalctl utility). AI agents should use this group of endpoints for the full cycle: "pull code from git → install dependencies → start the service → read logs".
Requirements: server in BLACKHOLE mode, running status, blackholeStatus: CONNECTED. In OPEN mode the Deploy API returns NOT_BLACKHOLE.
There is no need to wake a sleeping standalone virtual machine in advance: a deploy, a command, a file upload and a journal read wake a server in sleeping status on their own and wait until it is ready within the same request — up to 6.5 minutes. If the wake does not complete in that window, the request returns 503 WAKE_TIMEOUT, the server returns to sleeping, and a repeat request is safe. Details — Full application deploy, the "Known specifics" section. A sleeping server with a live plan-change operation (resize) returns 409 EXEC_BUSY before waking; wait for that operation to finish.
This is the contract for a Black Hole VM (
kind: "STANDALONE"). For a Galaxy application (kind: "GALAXY_APP") theCONNECTEDrequirement does not apply — the deploy builds the container itself. The full model and deploy contract for a Galaxy application — Galaxy app.
Galaxy application OOM → release to a dedicated server. If a Galaxy application deploy fails with
502 GALAXY_APP_START_FAILEDspecifically due to OOM (the application outgrew the container memory limit of 512 MB), the response contains a structured hinterror.hintwithrecoveryAction: "graduate-to-dedicated-vm". The recommended action is to recreate the application on a separate virtual machine:POST /v1/infra/serverswithplacement: "dedicated"andgraduateFrom(the identifier of the failed Galaxy application, which will be deleted after the server is created), thenPOST /v1/infra/servers/:id/deploywith the same source code. Duplicate-creation protection via theIdempotency-Keyheader does not extend to this release — together withgraduateFromit returns400 IDEMPOTENCY_UNSUPPORTED_WITH_GRADUATION. This is allowed when the Bitrix24 account'sserverCreationpolicy permits creating servers and you are within quota. A dedicated virtual machine is billed (it sleeps when idle). The hint is added only when the cause is OOM, not on an ordinary crash.
Response format: all endpoints return JSON by default (201/200 with {"success": true, ...}). For /deploy and /exec a streaming mode is available via ?stream=true — an SSE (Server-Sent Events) stream with line-by-line progress is returned. For AI agents and MCP clients always use JSON (do not add ?stream=true) — they cannot parse SSE.
Rate limits:
| Limit | Value |
|---|---|
| LimitOperations per minute per API-key-and-server pair | Value10 |
LimitConcurrent exec/deploy |
Value1 per server |
Limitexec timeout |
Value1–600 seconds (default 300) |
LimitInline body size (base64 in /upload, /deploy source.content) |
Value96 MB of body, about 72 MB of archive. Over the cap — 413 INLINE_SOURCE_TOO_LARGE |
LimitFile size via URL (/upload url, /deploy source.url) |
Value500 MB |
LimitArchive size of a saved version (/deploy source.versionId) |
Value500 MB |
LimitMultipart archive size in /deploy |
Value500 MB as long as the archive streams into storage. Otherwise about 72 MB — conditions |
Scope: vibe:infra
Run a command
POST /v1/infra/servers/:id/exec
Runs a shell command on a BLACKHOLE server through the tunnel agent. The full standard output (stdout), error stream (stderr), and return code are passed to the client. By default the response is JSON, returned after the command finishes (recommended for AI agents and scripts). On STANDALONE, if you need line-by-line progress, pass ?stream=true and read the stdout/stderr/exit SSE events. A failure during command execution arrives in streaming mode as an error event.
For AI agents and MCP clients, always use JSON mode (without ?stream=true) — they cannot parse SSE. The official Vibecode MCP client (the manage_server_deploy tool) drops this parameter itself and always works in JSON mode.
Streaming gives you live progress, but it does not raise the execution time limit: the synchronous execution limit is the same in JSON and streaming modes and equals timeout + 30 seconds, while timeout itself is capped at 600 seconds. On STANDALONE, for longer work with a retained result, use async=true — see "Durable exec" below. A client-managed background job is another option.
Which servers it runs on
The server kind arrives in the kind field of the GET /v1/infra/servers and GET /v1/infra/servers/:id responses.
kind |
Command runs | What to keep in mind |
|---|---|---|
kindSTANDALONE — a standalone virtual machine |
Command runsyes | What to keep in mindThe contract of this page applies in full, including workdir and env |
kindGALAXY_APP — an application in a galaxy |
Command runsyes, inside the application container | What to keep in mindThe workdir and env fields are not supported — 400 GALAXY_EXEC_NO_WORKDIR_ENV. Use cd /opt/app; FOO=bar node script.js only for non-secret values. Load secrets inside the container from an existing protected file or the application’s configured secret store; keep their values out of command text. Failures during execution arrive with status 502, not 200. There are two exceptions: EXEC_BUSY arrives as 409, and EXEC_NO_EXIT arrives as 200 with success: false, because it is not a channel failure but a completed call with no verdict (see below) Only synchronous exec is supported, with timeout up to 600 seconds. async=true receives 400 DURABLE_EXEC_UNSUPPORTED_SERVER_KIND without execution or fallback. |
kindGALAXY — the machine carrying application containers |
Command runsno — 403 GALAXY_HOST_EXEC_FORBIDDEN |
What to keep in mindThe machine carries containers belonging to different keys of a single Bitrix24 account, so it is not a command target. Run the command on the application, by its own ID (kind equals GALAXY_APP). For the machine's free disk space, look at the galaxy card in your Vibecode account — see Galaxy app Durable requests receive 400 DURABLE_EXEC_UNSUPPORTED_SERVER_KIND before dispatch. |
Parameters
| Parameter | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
Parameterid |
Inpath | Typestring (UUID) | Requiredyes | Default— | DescriptionBLACKHOLE server ID, status: running, blackholeStatus: CONNECTED |
Parameterstream |
Inquery | Typestring | Requiredno | Default— | Descriptiontrue — synchronous SSE mode on STANDALONE. Omit for JSON. Galaxy exec returns blocking JSON without a live SSE stream |
Parameterasync |
Inquery | Typestring | Requiredno | Defaultfalse |
Descriptiontrue — standalone durable execution with a retained result and a 202 response. Other kinds: 400 DURABLE_EXEC_UNSUPPORTED_SERVER_KIND, without synchronous fallback. Cannot be combined with stream=true |
Request fields (body)
| Field | Type | Required | Description |
|---|---|---|---|
Fieldcommand |
Typestring | Requiredyes | DescriptionShell command. 1–10,000 characters |
Fieldtimeout |
Typenumber | Requiredno | DescriptionExecution timeout in seconds: 1–600 synchronously; 1–86400 with async=true on STANDALONE only. Default 300 |
Fieldworkdir |
Typestring | Requiredno | DescriptionStandalone only. Up to 500 characters. Must be an existing directory whose cleaned absolute path is /opt, /srv, /tmp, /root, /var/log, /var/lib, /etc/systemd, /etc/nginx, or below one of them. Relative paths, remaining .. in the cleaned path, and sensitive prefixes listed below are denied. Omit to use the execution process’s default directory |
Fieldenv |
Typeobject | Requiredno | DescriptionEnvironment variables: { "KEY": "value" }. Only string values |
OpenAPI validation. The standard requestBody permits timeout up to 86400 seconds so generic validators and generated clients accept supported async=true requests on STANDALONE. The body schema does not account for the async query parameter: the synchronous API still rejects timeout above 600 seconds even when the standard schema accepts the body. The mode-specific contracts are #/components/schemas/InfraSyncExecRequest (up to 600 seconds) and #/components/schemas/InfraDurableExecRequest (up to 86400 seconds). The x-vibe-request-schema extension validates assembled {query,body} by mode; server kind is checked at runtime.
The x-vibe-request-schema extension validates the assembled {query, body} object: mode-dependent timeout bounds and the allowed async/stream combination. The API checks server kind at request time; choosing the durable body schema does not remove the STANDALONE prerequisite.
For workdir, path cleaning precedes validation. The following prefixes are denied even inside an allowed root: /etc/shadow, /etc/passwd, /etc/sudoers, /etc/cron, /etc/ssh/, /root/.ssh/, /root/.gnupg/, /root/.bashrc, /root/.profile, /root/.bash_history. A denied path causes EXEC_WORKDIR_DENIED; an unavailable directory causes EXEC_WORKDIR_NOT_FOUND, and a non-directory causes EXEC_WORKDIR_NOT_DIR. Durable admission can refuse a denied path with HTTP 502 before launch; an accepted job reports later directory checks through its operation. See durable outcome refusals.
Examples
curl — personal key
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/exec" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"command": "ls -la /opt/app", "timeout": 30}'
curl — OAuth application
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/exec" \
-H "X-Api-Key: YOUR_APP_KEY" \
-H "Authorization: Bearer USER_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": "npm ci --production", "workdir": "/opt/app", "timeout": 180}'
JavaScript — personal key
const res = await fetch(
`https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/exec`,
{
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
command: 'node -v',
timeout: 10,
}),
}
)
const { data } = await res.json()
console.log(`exit ${data.exitCode} in ${data.duration}ms:\n${data.stdout}`)
JavaScript — OAuth application
// A command with environment variables and a working directory
const res = await fetch(
`https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/exec`,
{
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_APP_KEY',
'Authorization': 'Bearer USER_SESSION_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
command: 'npx tsx scripts/seed.ts',
workdir: '/opt/app',
env: { DATABASE_URL: 'postgresql://localhost/mydb' },
timeout: 60,
}),
}
)
Response fields
| Field | Type | Mode | Description |
|---|---|---|---|
Fieldsuccess |
Typeboolean | ModeBoth | DescriptionSynchronous 200: true if the command ran, including a nonzero exit code. Durable 202: accepted for reconciliation; this does not prove launch |
Fielddata.exitCode |
Typenumber | ModeSynchronous 200 |
DescriptionProcess exit code |
Fielddata.stdout |
Typestring | ModeSynchronous 200 |
DescriptionStandard output |
Fielddata.stderr |
Typestring | ModeSynchronous 200 |
DescriptionError stream |
Fielddata.duration |
Typenumber | ModeSynchronous 200 |
DescriptionExecution time in milliseconds |
Fielddata.truncated |
Typeboolean | ModeSynchronous 200 |
DescriptionWhether output reached the shared 1 MiB (1,048,576 bytes) stdout+stderr limit |
Fielddata.operationId |
Typestring | ModeBoth | DescriptionRun identifier; on refusal use error.operationId. Required in durable 202: no dispatch without a stored record. Synchronous execution can continue without it if recording fails |
Fielddata.status |
Typestring | ModeDurable 202 |
Descriptionrunning: launch acknowledgement may be unconfirmed; poll the same operation |
Fielddata.statusUrl |
Typestring | ModeDurable 202 |
DescriptionURL for reading this operation and its retained result |
The durable 202 response contains no command output or exit code. Read retained fields through the authorized operation outcome endpoint.
Response example
{
"success": true,
"data": {
"exitCode": 0,
"stdout": "Linux epd65hdv07p8g89c0c06 6.8.0-107-generic #107-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 13 19:51:50 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux\n",
"stderr": "",
"duration": 5,
"truncated": false,
"operationId": "clz9k2m4x0001qw8h3f7d2n5q"
}
}
Standalone durable command accepted (202):
{
"success": true,
"data": {
"operationId": "clz9k2m4x0001qw8h3f7d2n5q",
"status": "running",
"statusUrl": "/v1/infra/operations/clz9k2m4x0001qw8h3f7d2n5q"
}
}
Error response example
409 EXEC_BUSY — exec admission is occupied. A platform-lock refusal returns the retry signals shown in this example:
{
"success": false,
"error": {
"code": "EXEC_BUSY",
"message": "Another operation is running on this server",
"retryable": true,
"retryAfter": 10
}
}
When the response includes retryable: true and retryAfter, it also carries the Retry-After HTTP header with the same interval in seconds. Follow that interval; error.hint.autoExpiresInSeconds, when present, gives the lock's full upper bound. A durable agent refusal instead returns error.operationId without retryable, retryAfter, or Retry-After. Read that operation before another exec request; an unknown outcome requires reconciliation and independent confirmation that the command has stopped.
Errors
The endpoint's errors fall into two groups, and they are delivered differently. A failure before the command starts arrives with a real HTTP status. A failure during execution arrives in the response body on a standalone virtual machine (kind: "STANDALONE"). On a Galaxy application (kind: "GALAXY_APP") it arrives with the status 502, and there are two exceptions there: a busy exec channel (EXEC_BUSY) arrives as a real 409, because it is a "busy" refusal rather than a failure, and EXEC_NO_EXIT arrives with 200 and success: false.
Errors before the command starts
| HTTP | Code | Description |
|---|---|---|
| HTTP400 | CodeVALIDATION_ERROR |
DescriptionThe request schema is violated (empty command, invalid timeout, workdir longer than 500 characters, invalid async value or async=true combined with stream=true). |
| HTTP400 | CodeDURABLE_EXEC_UNSUPPORTED_SERVER_KIND |
Descriptionasync=true targets a server whose kind is not STANDALONE, including GALAXY_APP and GALAXY. Refused before locking, operation creation or command dispatch; no operation ID or synchronous fallback. Galaxy remains available through a separate synchronous request with timeout up to 600 seconds |
| HTTP400 | CodeNOT_BLACKHOLE |
DescriptionServer in OPEN mode — the Deploy API is unavailable |
| HTTP400 | CodeCOMMAND_TOO_LONG |
DescriptionThe command is longer than 10,000 characters. The response carries a hint: ship large payloads and scripts via /upload, then run bash /path/script.sh |
| HTTP400 | CodeGALAXY_EXEC_NO_WORKDIR_ENV |
DescriptionA command for a Galaxy application (kind: "GALAXY_APP") carried workdir or env. Container execution does not accept them — use cd and non-secret assignments such as FOO=bar; load secrets from an existing protected file or the application’s configured secret store inside the container |
| HTTP401 | CodeMISSING_API_KEY |
DescriptionThe X-Api-Key header was not provided |
| HTTP401 | CodeINVALID_API_KEY |
DescriptionThe key is not recognized — no such key exists on the platform |
| HTTP402 | CodeACCOUNT_FROZEN |
DescriptionThe Vibecode balance is frozen. The request is rejected before the operation and a sleeping server is not woken — top up the balance and repeat |
| HTTP403 | CodeSERVER_WAKE_BLOCKED |
DescriptionThe server is asleep and waking is blocked — for example, platform access has ended or an administrative block is in place. A command does not bring such a server up, and a retry will not help until the block is lifted. For details on the block, see Wake a server |
| HTTP403 | CodeWRONG_KEY |
DescriptionThe server exists but is not yours. Operations on the application's content — deploy, exec, upload and log reads — are open on any one of three grounds: the server's managing key, a key whose application is bound to that server (Application.serverId), or membership in that server's development team (both roles, Developer and Administrator). Access tokens and the icon upload require the managing key. Managing the machine itself is also open to the Administrator role on the team. The response carries a hint with a two-step recovery. See Server access recovery |
| HTTP403 | CodeGALAXY_HOST_EXEC_FORBIDDEN |
DescriptionThe command was sent to the ID of a galaxy host machine (kind: "GALAXY"). Run it on the application, by its own ID — see "Which servers it runs on" above |
| HTTP403 | CodeINFRA_FORBIDDEN_FOR_COWORK_KEY |
DescriptionThe call was made with a Cowork/Code key — such a key works with data only and cannot perform write operations. To issue a key that can, see Project key for deploy |
| HTTP403 | CodeEXTERNAL_COLLABORATOR_KEY_SERVER_MISMATCH |
DescriptionThe call was made with an external-collaborator key, and the address names a server other than the one the key was issued for. Such a key is valid for exactly one server, and the response does not reveal whether the requested server exists. Working with another server requires the key issued for that external membership |
| HTTP403 | CodeEXTERNAL_COLLABORATOR_NOT_A_MEMBER |
DescriptionAn external-collaborator key addresses its own server, but access is not granted right now: the membership was removed, it expired, or external development collaborators are switched off for the Bitrix24 account that owns the server. The code does not tell these reasons apart and reveals nothing about the server. Membership is re-read on every request, so revoked access stops working immediately |
| HTTP404 | CodeNOT_FOUND |
DescriptionThere is no server with this id, or it was deleted. A server that exists but is not yours answers 403 WRONG_KEY — the code tells "no such server" apart from "no rights to it" |
| HTTP409 | CodeEXEC_BUSY |
DescriptionA sleeping server with a live plan-change operation (resize) returns 409 EXEC_BUSY before waking; wait for that operation to finish. Another command or deploy holds admission. A platform lock refusal carries Retry-After and retryable: true / retryAfter; follow that interval. A standalone durable start refused by the agent also returns 409 with error.operationId, but without these retry fields. Read the operation before retrying. A running standalone durable job normally keeps the channel busy after 202, without autoExpiresInSeconds; poll its result instead of calling /unstick or deleting the lock. On Galaxy the channel is shared by every application and deploy on the host. For an unknown outcome, reconcile state and logs and contact support; an absent error.hint.holder does not prove the host is idle. Lock recovery is covered by /unstick and DELETE /lock and requires independently confirmed idle state. Galaxy owners cannot force recovery |
| HTTP409 | CodeSERVER_NOT_READY |
DescriptionThe server is not ready for the operation: it is not running, the tunnel is not connected, or a successful non-empty Gateway snapshot confirms that a tunnel stored as CONNECTED is absent. This response is returned only when repair is not confirmed successful; an unavailable or empty snapshot does not confirm absence and the API uses the stored status. The platform attempts to restore a confirmed-missing tunnel before starting the command; if it cannot, the response carries a hint. This code does not describe a sleeping server — the platform wakes that one itself. Restore the tunnel with /repair, then repeat the request A sleeping server with a live plan-change operation (resize) returns 409 EXEC_BUSY before waking; wait for that operation to finish. |
| HTTP409 | CodeWAKE_IN_PROGRESS |
DescriptionAnother request is already waking this sleeping server. Wait for it to finish and repeat |
| HTTP422 | CodeVM_MISSING |
DescriptionThe server record has no virtual machine on the provider side, so there is nothing to wake. Replace the server: delete it and create a new one. error.hint names the endpoint of each step only if that step is available to this request, and for an unavailable step it gives the reason for the refusal and what to change. The error.userMessage field arrives only when both steps are available |
| HTTP422 | CodeVM_POINTER_LOST |
DescriptionThe record has no externalId, but the cloud review does see a machine under this server's cloud name: the link is lost while the machine itself is most likely running and billed. Do NOT delete the server and create a new one — that would leave the machine running with no record at all. The server is not moved to error; contact support to have the link restored |
| HTTP502 | CodeWAKE_FAILED |
DescriptionServer readiness could not be confirmed. This response does not mean that the server was stopped. Check the current server state before taking another action |
| HTTP502 | CodePROVIDER_ERROR |
DescriptionThe cloud provider returned an error while starting the sleeping server's virtual machine. The tunnel is not involved here, so /repair will not help |
| HTTP409 | CodeGALAXY_APP_NOT_READY |
DescriptionGalaxy applications only (kind: "GALAXY_APP"): the platform did not resolve the name of the application's container on the host and did not run the command. Two conditions. The container name failed validation — message then reads invalid on-host name. Or the application has no subdomain or no link to its galaxy host — message then reads Galaxy app is missing its subdomain or host link. Read the application's current state with GET /v1/infra/servers/:id |
| HTTP429 | CodeRATE_LIMITED |
DescriptionThe limit of 10 operations per minute per API-key-and-server pair was exceeded. The exact value arrives in the x-ratelimit-limit header (the cap is divided across replicas) |
| HTTP503 | CodeEXEC_JOB_STORAGE_UNAVAILABLE |
DescriptionA durable operation record or its target could not be stored; no command was dispatched |
| HTTP502 | CodeEXEC_JOB_CAPACITY |
DescriptionDurable admission reached 64 retained job directories on the host. The command did not start. Read error.operationId; wait for eligible seven-day retention cleanup or contact support. No automatic resend |
| HTTP502 | CodeEXEC_JOB_UNAVAILABLE |
DescriptionDurable storage, admission or systemd launch is unavailable. Read error.operationId and contact support; do not substitute a synchronous execution |
| HTTP502 | CodeEXEC_JOB_INVALID |
DescriptionThe agent rejected invalid durable launch data or an elapsed launch window. The command did not start. Read error.operationId; check the recorded refusal and contact support if a valid request keeps failing |
| HTTP502 | CodeEXEC_WORKDIR_DENIED |
DescriptionThe standalone working directory violates the path policy above. No command ran. Read error.operationId, then choose an allowed existing directory |
| HTTP502 | CodeEXEC_JOB_REJECTED |
DescriptionThe agent refused before execution without naming a code. Read the failed operation and contact support; do not substitute a synchronous launch |
| HTTP502 | CodeUNKNOWN_TYPE |
DescriptionThe agent does not support durable exec; command refused before launch, without synchronous fallback |
| HTTP503 | CodeWAKE_NO_CAPACITY |
DescriptionStartup has already been refused for lack of capacity. Read the current server state before trying again |
| HTTP503 | CodeWAKE_TIMEOUT |
DescriptionThe sleeping server did not come up within the allotted 6.5 minutes — either the machine never started or the tunnel never connected. The server returns to sleeping, so a repeat request is safe. See "Known specifics" |
Refusals after durable acceptance
HTTP 202 can precede a refusal detected before the user command starts. Poll the returned operation; status: "failed" and data.error.code identify a proven refusal, while unknown does not authorize another exec. An expired launch window or a refused working directory is described in the durable refusal table. An unknown outcome has data.error: null; do not infer success from the absence of an error code.
Errors during synchronous execution
The HTTP status does not reflect the command's outcome. On a standalone virtual machine (
kind: "STANDALONE") the connection is held open for the whole run, and the200status is sent before execution begins — so both a successful and a failed command arrive with the code200. The failure marker is thesuccess: falsefield and theerrorobject in the response body. Checksuccessin the body, not the HTTP status: a client that branches on the status will treat a failed command as successful. For a Galaxy application (kind: "GALAXY_APP") the same error arrives with the status502. There are two exceptions, the same ones as in the server-kind table above:EXEC_BUSYarrives as409, andEXEC_NO_EXITarrives as200withsuccess: false. Sosuccessin the body must be checked on both server kinds.
| Code | Description |
|---|---|
CodeEXEC_TIMEOUT |
DescriptionThe timeout was exceeded (or the default 300 seconds). The agent terminates the whole process group forcibly (SIGKILL, no grace period). The response carries a hint object — its fields are described below |
CodeEXEC_WORKDIR_DENIED |
DescriptionThe standalone working directory violates the path policy above. The agent refused before starting the command; reconcile the recorded outcome before using an allowed directory |
CodeEXEC_WORKDIR_NOT_FOUND |
DescriptionThe standalone working directory is absent or cannot be inspected. The agent refused before starting the command; reconcile the recorded outcome before using an existing accessible directory |
CodeEXEC_WORKDIR_NOT_DIR |
DescriptionThe standalone working path is not a directory. The agent refused before starting the command; reconcile the recorded outcome before using a directory |
CodeEXEC_FAILED |
DescriptionAn execution error on the agent |
CodeEXEC_NO_EXIT |
DescriptionThe stream ended without sending an exit status. What the command managed to do on the server is unknown, so this is not a success: the agent may have stopped responding, the tunnel may have dropped mid-command, or very large output may have overrun the channel after the command had already finished. data carries the output collected up to the break (stdout, stderr); the exitCode, duration and truncated fields are absent — the platform does not know their values. Returned on both server kinds. On a standalone virtual machine (kind: "STANDALONE") the response additionally carries a hint with the recovery path, a Galaxy application gets none: the hint names virtual-machine recovery, and an application inside a galaxy has no machine of its own |
CodeEXEC_BUSY |
DescriptionThe agent execution channel is occupied by a synchronous command, or on a standalone server by a durable job. The synchronous refusal is success: false with HTTP 200 on STANDALONE, or 409 with Retry-After and retryable: true / retryAfter on GALAXY_APP. Its hint.recoveryAction points first to GET /v1/infra/servers/:id/operations?kind=exec. Read durable outcomes and poll running jobs; an unknown outcome needs support and full-stop confirmation. An absent autoExpiresInSeconds does not prove a leaked mutex. Use /unstick only for a confirmed leaked synchronous mutex on an independently confirmed idle standalone server. A Galaxy owner cannot recover the shared host channel; another application’s job may be invisible in their list, so contact support if contention persists |
CodeCONTAINER_EXEC_OPTIONS_DENIED |
DescriptionSynchronous Galaxy exec refused host workdir or env options before execution. The public API normally catches these options earlier with 400 GALAXY_EXEC_NO_WORKDIR_ENV. Reconcile the recorded outcome and correct the synchronous request |
CodeCONTAINER_NOT_READY |
DescriptionThe galaxy host answered, but the application container the command was meant to run in is not there yet: the slot was created and never deployed, a deploy is in flight, or the container is restarting. Arrives with the status 502. The response carries a hint that leads to deploying the application rather than to the host — the host is healthy, so waking or repairing it changes nothing. Applies to a Galaxy application (kind: "GALAXY_APP") only |
The `hint` fields on `EXEC_TIMEOUT`
| Field | Type | Description |
|---|---|---|
Fieldhint.reason |
Typestring | DescriptionWhat happened: the command did not finish within the allotted time, and the whole process group was terminated immediately, with no grace period |
Fieldhint.recovery |
Typestring | DescriptionWhat to do: in synchronous mode the upper bound for timeout is 600 seconds, so run longer operations as a background job (systemd-run --unit=<name>) and follow it via GET /v1/infra/servers/:id/logs with the ?service=<name> parameter; for progress within the timeout pass ?stream=true |
Fieldhint.recoveryAction |
Typestring | DescriptionA pointer to the documentation: docs: /docs/infra/deploy/exec |
The hint arrives in the JSON response and the SSE error event. Agent-origin EXEC_BUSY uses GET /v1/infra/servers/:id/operations?kind=exec as its recoveryAction on both supported server kinds. Read the operation outcome first. Only a confirmed leaked synchronous mutex on an independently confirmed idle standalone server calls for /unstick. On STANDALONE, a running durable job keeps the execution channel busy after 202 releases the platform lock. Poll its operation; an unknown outcome needs support and independently confirmed full termination. Missing autoExpiresInSeconds does not prove a leaked mutex. Tunnel recovery and lock deletion do not clear durable admission. Galaxy commands run synchronously with a 600-second limit and share the host channel across all applications and deploys; your own operation list may not show another application’s job, so an empty list does not prove the host is idle.
A tunnel communication failure arrives in the same shape — with its own code in the error.code field (for example TUNNEL_NOT_FOUND or GATEWAY_TIMEOUT: …). Some gateway codes carry details after a colon (GATEWAY_UNREACHABLE: …, GATEWAY_TIMEOUT: …), so match error.code by prefix, not by an exact comparison. In streaming mode (?stream=true) the same errors arrive as an SSE error event. The event data carries code and message, and for EXEC_TIMEOUT, GATEWAY_TIMEOUT and an agent-side EXEC_BUSY it also carries a hint. The GATEWAY_TIMEOUT hint states the key point: no exit status came back, so the outcome of the command is unknown and it may still be running on the server — do not re-run it blindly, check the state first. These events carry no success field — the error event itself is the failure marker.
Full list of common API errors — Errors.
Known specifics
- A sleeping standalone virtual machine is woken by the command itself. If the server (
kind: "STANDALONE") is insleepingstatus, the platform starts the wake and waits for it within the same request — up to 6.5 minutes — and only then runs the command. A separate/wakecall is not needed, but the client's request timeout must be longer than that window. If the server does not come up,503 WAKE_TIMEOUTarrives and the server returns tosleeping— the command never ran, so a repeat request is safe. A sleeping server with a live plan-change operation (resize) returns409 EXEC_BUSYbefore waking; wait for that operation to finish. - The command runs through
/bin/sh. Thecommandfield is executed by the/bin/shinterpreter — a minimal POSIX shell (dash), notbash. Constructs specific tobash(set -o pipefail,[[ … ]], arrays) are not available there and fail with an error. Run them explicitly — viacommand: "bash -c 'set -o pipefail; …'"or as a script filecommand: "/bin/bash /opt/app/script.sh". - Server-level locking. Synchronous
/execand/deployacquire a platform lock; synchronous exec has atimeout + 90second lock window and deploy has a 15-minute window. Standalone durable exec releases that platform lock after202, but keeps its execution channel busy throughout the job, with requested timeout up to 86400 seconds. Completion checks can extend admission beyond the command timeout. An unknown outcome needs reconciliation and confirmed termination;/unstickandDELETE /lockdo not clear the durable gate. Galaxy exec is synchronous and capped at 600 seconds, and its host channel is shared by all applications and deploys. Recover a leaked synchronous lock only after independently confirming idle server state and logs. - The total output limit is 1 MiB (1,048,576 bytes), shared across
stdoutandstderr. It is one budget for both streams, not 1 MiB each: if the command writes to bothstdoutandstderr, truncation is measured against their combined size. When the output is larger than the limit,truncated: truearrives and the tail is cut off. The command still ran to completion, andexitCodeis correct. For large output, redirect to a file:command: "my-cmd > /opt/app/output.log 2>&1"and then read it via/exec cat /opt/app/output.log. - The
.envfrom/deployis not picked up automatically. Systemd loads.envwhen the service starts, but not for one-off commands via/exec. If you needDATABASE_URL/ API keys, pass them in theenvfield:{ env: { DATABASE_URL: "..." } }. - The two timeouts together give
timeout + 30seconds. The agent kills the process exactly attimeout; the Gateway waits another 30 seconds for the final events and only then returnsEXEC_TIMEOUT. - Keeping the connection alive in JSON mode. If the command runs longer than 15 seconds, the server periodically sends spaces in the response body — this prevents timeouts in nginx and intermediate proxies. The spaces go inside the already-opened JSON object, so the body looks like JSON (
{) from its very first byte instead of starting with spaces: strict clients and ordinary parsers read it the same way, and you do not need to trim anything by hand. - Client timeout: make sure it is inter-byte, not total. The keepalive spaces reset "silence" timeouts, so the classic Python socket timeouts (
urlopen(..., timeout=300), therequestsread timeout) will not by themselves abort a long command in JSON mode. Two other things do abort it. First — clients and wrappers with a true wall-clock deadline for the whole request (for example,aiohttpwithClientTimeout(total=…)or an agent framework's own deadline): keepalive cannot help against those. Second — the server-sidetimeoutof the command itself (300 seconds by default): once it expires, the agent kills the process and you getEXEC_TIMEOUTregardless of the client settings — do not mistake it for a client-side disconnect. In practice: in Python set split timeouts —requests.post(url, json=body, headers=headers, timeout=(10, 60))— 10 seconds for connect and 60 for the inter-byte pause. In synchronous mode, if the client drops the connection, the server-side lock persists for up totimeout + 90seconds (EXEC_BUSYfor repeated calls). Commands longer than a few minutes are better run as a background job — see "Background jobs" below. On a sleeping server this rule does not apply right away: the spaces start only after the wake, so up to 6.5 minutes of complete silence precede the first byte of the response, and thetimeout=(10, 60)recipe aborts the request before the command even starts. Either raise the inter-byte timeout above that window, or wake the machine in advance with/wakeandwait=true, then call/execon a machine that is already up. A sleeping server with a live plan-change operation (resize) returns409 EXEC_BUSYbefore waking; wait for that operation to finish. Do not use manual wake to bypass resize.
Unsticking a stuck channel
On STANDALONE, an EXEC_BUSY without autoExpiresInSeconds can mean a durable job is still running; it does not prove a leaked agent mutex. First read the operation outcome, or discover the run in the server operation list with kind=exec. Wait while it is running. An unknown outcome does not confirm termination: reconcile state and logs and contact support. Do not bounce the tunnel or delete the lock for a running or unconfirmed durable job; /unstick does not clear its admission gate. Use POST /v1/infra/servers/:id/unstick only for lock recovery after independently confirming idle state, subject to that endpoint’s guards.
Choosing the mode
Four call modes and when each one fits:
| When | How | Monitoring |
|---|---|---|
| WhenThe command fits into 600 seconds, no progress needed | How{ "command": "...", "timeout": 600 } without ?stream=true — a blocking JSON response |
MonitoringexitCode, stdout, stderr in the response body |
WhenA standalone command fits into timeout, live progress needed |
Howthe same request with ?stream=true — SSE events stdout / stderr / exit |
Monitoringread the events as they arrive |
| WhenRetained standalone result; its channel stays busy throughout the job | How?async=true, timeout up to 86400 on STANDALONE only |
MonitoringPoll statusUrl for the outcome and retained stdout/stderr |
| WhenA standalone command whose channel must be released before the job finishes | Howa client-managed background job systemd-run --unit=<name> — /exec returns immediately |
MonitoringGET /v1/infra/servers/:id/logs?service=<name> |
For synchronous exec, do not set the client-side overall request deadline lower than the command's timeout — the client would cut the connection before the server can answer. A sleeping server adds up to 6.5 minutes of wake time to that deadline — the example below assumes a machine that is already up. An example of split timeouts in Python:
import requests
r = requests.post(
f"{VIBE_URL}/v1/infra/servers/{SERVER_ID}/exec",
headers={"X-Api-Key": VIBE_API_KEY},
json={"command": "npm ci --production", "workdir": "/opt/app", "timeout": 600},
timeout=(10, 60), # 10 seconds to connect, 60 for a pause between bytes
)
Raising the timeout of /exec does not help the install step — deploy steps have their own 300-second limit, separate from the synchronous 600-second /exec limit.
Important: the timeout default is 300 seconds, not 600. The synchronous 600-second ceiling exists, but to use it the field must be passed explicitly. A command planned for nine minutes and started without timeout is killed at the fifth minute.
If the wait was interrupted
A transport timeout, a dropped connection, or a client restart end only the wait for the response. The command on the server is not stopped and can run to completion after the client already received an error. That is why re-running a state-changing command may apply the change a second time.
To avoid that, an accepted run usually gets an identifier — but in synchronous mode that is not a guarantee: when the platform could not open the record there is none, and the command still runs. Standalone durable execution requires a stored record and returns the identifier with 202. When it exists, it arrives through three channels:
- the
X-Vibe-Operation-Idresponse header — on a standalone virtual machine it is sent before the command starts executing, so it survives to the client even when the response body is lost. For synchronous Galaxy exec it arrives only with the terminal response: that branch opens no stream, so an interrupted wait never receives the id and the run is found in the server operation list with?kind=exec, - the first
event: operationframe with?stream=true— synchronous exec on standalone virtual machines only. A browserEventSourcedoes not expose response headers, so this is its only early channel, - the
operationIdfield in the body —data.operationIdon success,error.operationIdon failure.
The recovery order:
- Do not re-run a state-changing command blindly.
- Call
GET /v1/infra/operations/:operationId. If the identifier is lost, find the run in the server operation list with?kind=exec. - Status
succeededmeans the command ran to completion, and its own verdict is inexitCode. There is nothing to repeat. - Status
failedis set by the platform only when it can prove the command never ran — for example onEXEC_BUSY. That proves only this attempt never ran. ForEXEC_BUSY, first reconcile the occupying operation, including any earlier attempt whose acknowledgement was lost; an unknown occupying outcome does not authorize another exec. - Status
unknownmeans the outcome is not proven and the command may be running right now. Reconcile the server state and the logs before deciding to repeat it.
For synchronous exec, the record stores the outcome of the attempt — status, exit status and the reason for a refusal — but not command output. Use async=true to retain stdout/stderr with the operation; a client-managed background job with journal monitoring is another option.
Background jobs
For synchronous JSON and streaming exec, timeout is capped at 600 seconds, and when it expires the agent terminates the whole process group forcibly (SIGKILL, no grace period). On a standalone machine, for longer work — package installation, a database dump restore, a heavy build — prefer durable async=true when you need retained output. Alternatively, manage a background job yourself: /exec returns instantly, the channel is freed (a second call will not hit EXEC_BUSY), and the process keeps running independently of the HTTP connection.
Recommended — a transient systemd unit
{ "command": "systemctl reset-failed restore-db 2>/dev/null; systemd-run --unit=restore-db /bin/bash /opt/data/restore.sh", "timeout": 30 }
The job gets its own cgroup and lives independently of the exec session. Manage it with standard calls:
- status:
{ "command": "systemctl is-active restore-db" }—active(running),inactive(finished successfully),failed(crashed). - logs: journald picks up the unit's output automatically — read it via
GET /v1/infra/servers/:id/logs?service=restore-db. systemctl reset-failed <name>before a rerun — otherwise systemd refuses to create a unit with the name of the failed job.
Lightweight — background with output redirect
{ "command": "(cd /opt/app && npm run build > /tmp/build.log 2>&1 &)", "timeout": 10 }
Redirecting both streams to a file is mandatory — it releases the exec session's pipes. Progress: { "command": "tail -20 /tmp/build.log" }.
Anti-pattern — bare `nohup`
nohup cmd & without a redirect does not work: in a pipe (rather than a terminal) nohup does not redirect the output, the background process inherits the exec session's pipe, and the agent waits for it to close right up to the timeout — then forcibly terminates the whole process group, including your "background" process. Always add > file 2>&1.
Keep secret values out of command. On a standalone server, pass them via env; on a Galaxy application, load them inside the container from an existing protected file or the application’s configured secret store. Synchronous exec logs the first 200 characters of command text on the agent. The durable worker disables that command log, but commands can still expose secrets through their own output or child processes. Also keep in mind that the install/preStart steps of /deploy have their own 300-second timeout — move heavy installations into a one-off /exec or a background job, not into the install script.
Durable exec
Use POST /v1/infra/servers/:id/exec?async=true for a long command on a STANDALONE server only. It returns HTTP 202, X-Vibe-Operation-Id and data: {operationId, status: "running", statusUrl}. Poll GET /v1/infra/operations/:operationId; do not resend /exec after a lost acknowledgement. If the id was lost, discover the run through the server operation list with kind=exec.
The body accepts timeout from 1 to 86400 seconds (default 300). async=true cannot be combined with stream=true. Every other server kind receives 400 DURABLE_EXEC_UNSUPPORTED_SERVER_KIND before locking, operation creation or dispatch, without a synchronous fallback. Galaxy exec remains synchronous with a 600-second limit. An older execution service refuses durable exec before execution; no synchronous fallback runs.
The job survives a lost HTTP response and reconnection. Its execution channel remains busy throughout the job even after the platform releases its lock on returning 202; other exec calls and deploy commands receive EXEC_BUSY. Poll the operation instead of resending exec or calling /unstick. A completed operation returns stdout, stderr, duration in milliseconds, truncated and its proven exitCode, with a shared 1 MiB output cap. Output is encrypted at rest and retained within the seven-day outcome window. Private output is returned only to a scoped key owned by the operation creator; a same-owner rotation preserves access, a transfer to another owner does not. succeeded includes nonzero exit codes; unknown does not authorize a repeat. A host reboot or lost process can leave an unknown outcome requiring reconciliation and confirmed termination. Status reads reconcile the result without starting a command. Cancellation and automatic retries are unavailable.
The execution service retains at most 64 job records per standalone host, including completed jobs during their seven-day retention window. A new request at capacity is refused before execution with HTTP 502 EXEC_JOB_CAPACITY and error.operationId. Read that operation first, then wait for eligible retention cleanup or contact support; do not automatically resend. Cleanup runs on admission and removes only expired results with confirmed termination. If a job is interrupted before its result is saved, confirmed termination leaves its outcome unknown, but makes its record eligible for cleanup after seven days. Active jobs and unconfirmed termination are not cleared just because seven days elapsed. Neither /unstick nor lock deletion clears the durable admission gate.