For AI agents: markdown of this page — /docs-content-en/infra/deploy/exec.md documentation index — /llms.txt

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). 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 limit is the same in both modes and equals timeout + 30 seconds, while timeout itself is capped at 600 seconds. For work that clearly will not fit within that limit, start a background job — see "Background jobs" below.

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
STANDALONE — a standalone virtual machine yes The contract of this page applies in full, including workdir and env
GALAXY_APP — an application in a galaxy yes, inside the application container The workdir and env fields are not supported — 400 GALAXY_EXEC_NO_WORKDIR_ENV. Prefix the command itself instead: cd /opt/app; FOO=bar node script.js. Failures during execution arrive with status 502, not 200 — except EXEC_BUSY, which arrives as 409 (see below)
GALAXY — the machine carrying application containers no — 403 GALAXY_HOST_EXEC_FORBIDDEN The 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

Parameters

Parameter In Type Required Default Description
id path string (UUID) yes BLACKHOLE server ID, status: running, blackholeStatus: CONNECTED
stream query string no true — SSE streaming mode. For a JSON response, do not pass the parameter: JSON is returned by default

Request fields (body)

Field Type Required Description
command string yes Shell command. 1–10,000 characters
timeout number no Execution timeout in seconds: 1–600. Default 300
workdir string no Working directory. Up to 500 characters
env object no Environment variables: { "KEY": "value" }. Only string values

Examples

curl — personal key

Terminal
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

Terminal
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

javascript
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}s:\n${data.stdout}`)

JavaScript — OAuth application

javascript
// 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 Description
success boolean true if the command ran (including a non-zero exitCode)
data.exitCode number The process's return code
data.stdout string The command's standard output
data.stderr string The command's error stream
data.duration number Execution time in seconds
data.truncated boolean true if stdout/stderr were truncated at the size limit (5 MB per stream)

Response example

JSON
{
  "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
  }
}

Error response example

409 — another operation is already running on the server. This is a failure before the command starts, so the HTTP status reflects the outcome. A Galaxy application returns the same status when the host's shared exec channel is busy — that one arises during execution, but it means the same thing: busy, retry:

JSON
{
  "success": false,
  "error": {
    "code": "EXEC_BUSY",
    "message": "Another operation is running on this server",
    "retryable": true,
    "retryAfter": 10
  }
}

The response also carries a Retry-After HTTP header (in seconds, equal to the retryAfter field) — this is a short poll interval: keep retrying with it until the lock clears. The full upper bound — when the lock auto-expires — is in error.hint.autoExpiresInSeconds.

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 — with one exception: a busy exec channel (EXEC_BUSY) on a Galaxy application arrives as a real 409, because it is a "busy" refusal rather than a failure.

Errors before the command starts

HTTP Code Description
400 VALIDATION_ERROR The request schema is violated (empty command, invalid timeout, workdir longer than 500 characters)
400 NOT_BLACKHOLE Server in OPEN mode — the Deploy API is unavailable
400 COMMAND_TOO_LONG The 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
400 GALAXY_EXEC_NO_WORKDIR_ENV A command for a Galaxy application (kind: "GALAXY_APP") carried workdir or env. The container run does not accept them — prefix the command itself instead: cd /opt/app; FOO=bar node script.js
401 MISSING_API_KEY The X-Api-Key header was not provided
401 INVALID_API_KEY Invalid or expired API key
402 ACCOUNT_FROZEN The Vibecode balance is frozen. The request is rejected before the operation and a sleeping server is not woken — top up the balance and repeat
403 SERVER_WAKE_BLOCKED The server is asleep and waking is blocked by the platform — an ended trial or an administrative block. 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
403 WRONG_KEY The 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
403 GALAXY_HOST_EXEC_FORBIDDEN The 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
403 INFRA_FORBIDDEN_FOR_COWORK_KEY The 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
404 NOT_FOUND There 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"
409 EXEC_BUSY Another /exec or /deploy is already running on the server. The response carries a Retry-After header and retryable: true / retryAfter (seconds) fields — retry at that interval. Or release the lock via /lock. If EXEC_BUSY persists after releasing the lock, unstick the channel via POST /v1/infra/servers/:id/unstick. On a galaxy host and a Galaxy application unsticking is not available to the owner (409 GALAXY_UNSTICK_UNSUPPORTED): the exec channel is shared by every application on the host — retry after 30-60 s, and if it persists contact support
409 SERVER_NOT_READY The server is not ready for the operation: it is not running or the tunnel is not connected. The response carries a hint field with the reason and the next step. This code does not describe a sleeping server — the platform wakes that one itself. Start a stopped server with /start or restore the tunnel with /repair, then repeat the request. The "listed as connected but the Gateway has no live tunnel" case on this route arrives as 502 TUNNEL_NOT_FOUND (see below)
409 WAKE_IN_PROGRESS Another request is already waking this sleeping server. Wait for it to finish and repeat
422 VM_MISSING The server record has no virtual machine on the provider side, so there is nothing to wake. Delete the server and create a new one
502 WAKE_FAILED While the sleeping server was being woken it entered an unexpected state. Repeat the request
502 PROVIDER_ERROR The cloud provider returned an error while starting the sleeping server's virtual machine. The tunnel is not involved here, so /repair will not help
409 GALAXY_APP_NOT_READY Galaxy 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
429 RATE_LIMITED The limit of 10 operations per minute per server was exceeded
503 WAKE_TIMEOUT The 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"

Errors during 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 the 200 status is sent before execution begins — so both a successful and a failed command arrive with the code 200. The failure marker is the success: false field and the error object in the response body. Check success in 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 status 502.

Code Description
EXEC_TIMEOUT The 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
EXEC_FAILED An execution error on the agent
EXEC_NO_EXIT The 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. The response carries a hint with the recovery path. Applies to a standalone virtual machine (kind: "STANDALONE") only; on a Galaxy application this case still arrives as success: true with exitCode: -1
EXEC_BUSY The agent's own exec mutex is busy — it is already running another command. This differs from the platform lock (409 before the start): it arrives during execution, in the body as success: false (on STANDALONE — with HTTP 200, on GALAXY_APP — with 409, together with a Retry-After header and the retryable: true / retryAfter fields). The response carries a hint with the recovery path: on STANDALONE — via /unstick, on GALAXY_APP — retry and contact support, because an application owner cannot unstick the host's shared channel
CONTAINER_NOT_READY The 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
hint.reason string What happened: the command did not finish within the allotted time, and the whole process group was terminated immediately, with no grace period
hint.recovery string What to do: 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
hint.recoveryAction string A pointer to the documentation: docs: /docs/infra/deploy/exec

The hint arrives both in the JSON response and in the SSE error event of streaming mode. An agent-side EXEC_BUSY carries a hint object of the same shape — on a standalone virtual machine (kind: "STANDALONE") its recoveryAction points to POST /v1/infra/servers/:id/unstick. On a Galaxy application that path is closed to the owner, so its recoveryAction describes retrying and contacting support.

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 in sleeping status, 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 /wake call is not needed, but the client's request timeout must be longer than that window. If the server does not come up, 503 WAKE_TIMEOUT arrives and the server returns to sleeping — the command never ran, so a repeat request is safe.
  • The command runs through /bin/sh. The command field is executed by the /bin/sh interpreter — a minimal POSIX shell (dash), not bash. Constructs specific to bash (set -o pipefail, [[ … ]], arrays) are not available there and fail with an error. Run them explicitly — via command: "bash -c 'set -o pipefail; …'" or as a script file command: "/bin/bash /opt/app/script.sh".
  • Server-level locking. While an /exec or /deploy is running, a second such call returns 409 EXEC_BUSY. If the previous /deploy was interrupted (for example, failed at the healthcheck step or the client aborted the connection on a timeout), the server lock remains held until it expires automatically — for /deploy this is up to 15 minutes, for /exec — until the command's timeout plus 90 seconds has elapsed. The lock can be released immediately by calling DELETE /v1/infra/servers/:id/lock, after which /exec works again. There is no need to recreate the server.
  • The maximum size of stdout/stderr is 5 MB per stream. If the command's output is larger, truncated: true arrives and the tail is cut off. The command still ran to completion, and exitCode is 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 .env from /deploy is not picked up automatically. Systemd loads .env when the service starts, but not for one-off commands via /exec. If you need DATABASE_URL / API keys, pass them in the env field: { env: { DATABASE_URL: "..." } }.
  • The two timeouts together give timeout + 30 seconds. The agent kills the process exactly at timeout; the Gateway waits another 30 seconds for the final events and only then returns EXEC_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), the requests read 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, aiohttp with ClientTimeout(total=…) or an agent framework's own deadline): keepalive cannot help against those. Second — the server-side timeout of the command itself (300 seconds by default): once it expires, the agent kills the process and you get EXEC_TIMEOUT regardless 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. If the client does drop the connection, the server-side lock persists for up to timeout + 90 seconds (EXEC_BUSY for 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 the timeout=(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 /wake and wait=true, then call /exec on a machine that is already up.

Unsticking a stuck channel

If /exec still returns EXEC_BUSY after DELETE /v1/infra/servers/:id/lock (and /deploy fails with the code DEPLOY_FAILED and the message "Another command is running"), the agent-side exec mutex is stuck: the server lock is released, but a background process keeps the channel busy. That case is handled by POST /v1/infra/servers/:id/unstick — its parameters, response and every refusal code are covered there.

Choosing the mode

Three call modes and when each one fits:

When How Monitoring
The command fits into 600 seconds, no progress needed { "command": "...", "timeout": 600 } without ?stream=true — a blocking JSON response exitCode, stdout, stderr in the response body
The command fits into timeout, live progress needed the same request with ?stream=true — SSE events stdout / stderr / exit read the events as they arrive
The command runs longer than 600 seconds, or the channel must not stay busy for its whole duration a background job systemd-run --unit=<name>/exec returns immediately GET /v1/infra/servers/:id/logs?service=<name>

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:

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 600 seconds of /exec.

Important: the timeout default is 300 seconds, not 600. The 600 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.

Background jobs

The timeout of /exec is capped at 600 seconds, and when it expires the agent terminates the whole process group forcibly (SIGKILL, no grace period). Run anything longer — package installation, a database dump restore, a heavy build — as a background job: /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.

JSON
{ "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

JSON
{ "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.

Pass secrets (database passwords and the like) via the env field, not inside command — the agent logs executed commands (the first 200 characters). 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.

See also