For AI agents: markdown of this page — /docs-content-en/infra/deploy/operation-status.md documentation index — /llms.txt
Operation outcome
GET /v1/infra/operations/:operationId
Returns the outcome of one specific run by its operation id. The endpoint serves two kinds of operation — a deploy POST /deploy and a command POST /exec. Use it when the connection dropped and the terminal response never reached the client: the id tells you how the run ended instead of forcing a blind retry.
For a command this is the only way to learn the outcome after a drop. A transport timeout ends only the client-side wait. The command itself keeps running on the server and can finish after the client already received an error. That is why re-running a state-changing command may apply the change a second time.
The endpoint is read-only and has no side effects. Every run is addressable on its own, including earlier attempts on the same server. Outcomes are kept for 7 days.
Both deploy kinds receive an id, but at different points:
- for a standalone virtual machine (
kind: "STANDALONE"), the record is opened before the pipeline. If the machine is sleeping, the row is already visible asrunningin the server operation list while the blocking wake is in progress, but no HTTP status, header, or SSE frame reaches this connection until the wake completes. The header and first SSE frame arrive before the first deploy step; - for a Galaxy application (
kind: "GALAXY_APP"), the record is opened after the application mutex is acquired, on entry to the first step. The id arrives only with the terminal JSON response, in the header and body. If the connection dropped earlier, find the run through the server operation list.
For a command POST /exec the record is opened after the server lock is acquired and
before the command is dispatched to the agent. On a standalone virtual machine the header carrying
the id is sent before the wait begins. For a Galaxy application it arrives only with the terminal
response: that branch opens no stream and writes nothing into the connection until the run ends, so
an interrupted wait never receives the id — find the run through the
server operation list with ?kind=exec.
Where the id comes from — three channels, all carried by the same run:
- the
X-Vibe-Operation-Idresponse header — immediate for a command and for a deploy on an already available standalone machine, delayed until the blocking wake completes for a sleeping standalone deploy, and terminal-response-only for a Galaxy application in both cases; - the first
event: operationframe with?stream=true— for a command and for a deploy on a standalone machine, but only after any blocking wake; a browserEventSourcedoes not expose response headers, so this is its only pre-pipeline channel; - the
operationIdfield in the body —data.operationIdon success,error.operationIdon failure.
A missing id does not mean the deploy never started. A sleeping standalone connection may have dropped during the blocking wake, a Galaxy connection may have dropped before the terminal response, or the platform may have failed to open a record and continued without an id. Do not redeploy blindly: first call GET /v1/infra/servers/:id/operations, then reconcile the server state and logs.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
operationId |
path | string | yes | Operation ID from the POST /deploy or POST /exec response |
Examples
curl — personal key
curl -H "X-Api-Key: YOUR_API_KEY" \
https://vibecode.bitrix24.com/v1/infra/operations/OPERATION_ID
curl — OAuth application
curl -H "X-Api-Key: YOUR_APP_KEY" \
-H "Authorization: Bearer USER_SESSION_TOKEN" \
https://vibecode.bitrix24.com/v1/infra/operations/OPERATION_ID
JavaScript — personal key
// Deploy: after any blocking wake, take the id from the header BEFORE the body arrives.
// If the connection drops during wake, discover the id through the server operation list.
const deploy = await fetch(
`https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/deploy`,
{
method: 'POST',
headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({ source: { content: archiveBase64 }, start: 'node index.js' }),
}
)
const operationId = deploy.headers.get('X-Vibe-Operation-Id')
// The connection dropped — ask for the outcome instead of redeploying
const res = await fetch(
`https://vibecode.bitrix24.com/v1/infra/operations/${operationId}`,
{ headers: { 'X-Api-Key': 'YOUR_API_KEY' } }
)
if (res.status === 410) {
console.log('The operation existed, its outcome is no longer stored')
} else if (res.status === 404) {
console.log('No such operation')
} else {
const { data } = await res.json()
console.log(`Status: ${data.status}, step: ${data.step}`)
}
JavaScript — OAuth application
const res = await fetch(
`https://vibecode.bitrix24.com/v1/infra/operations/${operationId}`,
{
headers: {
'X-Api-Key': 'YOUR_APP_KEY',
'Authorization': 'Bearer USER_SESSION_TOKEN',
},
}
)
Response fields
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true on success |
data.operationId |
string | Operation ID |
data.kind |
string | Operation kind: deploy or exec |
data.serverId |
string | ID of the server the deploy ran on |
data.status |
string | running, succeeded, failed or unknown — see the table below |
data.step |
string | null | Pipeline step the operation is on, or stopped at. Always null for kind: "exec" — a command has no steps |
data.exitCode |
number | null | kind: "exec" only. Exit status of the command when the agent proved one, otherwise null. A number arrives only together with status: "succeeded" — see the pair rule below the status table |
data.startedAt |
string | Start time, ISO 8601 |
data.finishedAt |
string | null | Finish time, ISO 8601. null while the operation is running |
data.error |
object | null | { code, message } when status: "failed", otherwise null |
Values of data.status:
| Value | Meaning |
|---|---|
running |
The run is in progress right now |
succeeded |
For a deploy it means it finished successfully. For a command it means it ran to completion, and its own verdict is in data.exitCode — a non-zero exit status arrives with this status too, because the status answers "did it run", not "what was the result" |
failed |
The reason is in data.error. For a deploy it means the deploy failed. For a command it means the command never ran: the status is set only when the platform can prove that — for example on EXEC_BUSY, where the agent was busy with another command. Retrying the command is safe in that case |
unknown |
The platform cannot prove the remote outcome: the transport was lost or the process died between the start and the outcome write. Not the same as "there was no run": the operation definitely started. For a command it means the command may be executing on the server right now, so reconcile the server state in GET /v1/infra/servers/:id and the logs before re-running a state-changing command |
Judge by the pair data.exitCode + data.status, never by one field alone. A number arrives only with succeeded and means the command ran to completion. null on its own does NOT mean "the outcome is not proven": with succeeded it means the command ran but the agent named no code, so do not re-run it; with failed it means the command never started and a retry is safe; with running it means the run is still in flight; and only with unknown is nothing proven.
What is stored and what is not. The record carries the outcome of the attempt — status, exit status and the reason for a refusal. The command output (stdout and stderr) is not stored in it. If the output of a long command is needed after a drop, run it as a background job and read the journal through GET /v1/infra/servers/:id/logs — the procedure is described in the section on running commands.
Response example
Deploy in progress:
{
"success": true,
"data": {
"operationId": "clz9k2m4x0001qw8h3f7d2n5p",
"kind": "deploy",
"serverId": "8f14e45f-ceea-467a-9c8d-2f1c5a6b7e30",
"status": "running",
"step": "install",
"startedAt": "2026-08-11T09:14:22.117Z",
"finishedAt": null,
"error": null
}
}
Deploy finished successfully:
{
"success": true,
"data": {
"operationId": "clz9k2m4x0001qw8h3f7d2n5p",
"kind": "deploy",
"serverId": "8f14e45f-ceea-467a-9c8d-2f1c5a6b7e30",
"status": "succeeded",
"step": null,
"startedAt": "2026-08-11T09:14:22.117Z",
"finishedAt": "2026-08-11T09:18:04.902Z",
"error": null
}
}
Deploy failed:
{
"success": true,
"data": {
"operationId": "clz9k2m4x0001qw8h3f7d2n5p",
"kind": "deploy",
"serverId": "8f14e45f-ceea-467a-9c8d-2f1c5a6b7e30",
"status": "failed",
"step": "healthcheck",
"startedAt": "2026-08-11T09:14:22.117Z",
"finishedAt": "2026-08-11T09:17:41.338Z",
"error": {
"code": "DEPLOY_STEP_FAILED",
"message": "connect ECONNREFUSED 127.0.0.1:3000"
}
}
}
The command ran to completion:
{
"success": true,
"data": {
"operationId": "clz9k2m4x0001qw8h3f7d2n5q",
"kind": "exec",
"serverId": "8f14e45f-ceea-467a-9c8d-2f1c5a6b7e30",
"status": "succeeded",
"step": null,
"exitCode": 0,
"startedAt": "2026-08-11T09:20:02.004Z",
"finishedAt": "2026-08-11T09:24:37.815Z",
"error": null
}
}
The wait for the command was interrupted, the outcome is unknown:
{
"success": true,
"data": {
"operationId": "clz9k2m4x0001qw8h3f7d2n5r",
"kind": "exec",
"serverId": "8f14e45f-ceea-467a-9c8d-2f1c5a6b7e30",
"status": "unknown",
"step": null,
"exitCode": null,
"startedAt": "2026-08-11T09:31:10.220Z",
"finishedAt": "2026-08-11T09:36:41.905Z",
"error": null
}
}
Error response example
410 — the operation existed, but its outcome is no longer stored:
{
"success": false,
"error": {
"code": "OPERATION_OUTCOME_EXPIRED",
"message": "The outcome of this deploy is no longer stored (kept for 7 days).",
"serverId": "8f14e45f-ceea-467a-9c8d-2f1c5a6b7e30",
"startedAt": "2026-07-30T11:02:19.400Z"
}
}
Errors
| HTTP | Code | Description |
|---|---|---|
| 400 | INVALID_OPERATION_ID |
The ID is not in the format the platform hands out |
| 401 | MISSING_API_KEY |
The X-Api-Key header is missing |
| 401 | INVALID_API_KEY |
The key is not recognized — no such key exists on the platform |
| 404 | OPERATION_NOT_FOUND |
No such operation, it belongs to a different key, or the record has been removed |
| 410 | OPERATION_OUTCOME_EXPIRED |
The operation is yours, but its outcome is no longer stored |
| 429 | RATE_LIMITED |
Polling limit exceeded |
The full list of common API errors — Errors.
Known specifics
- Someone else's operation answers exactly like a non-existent one, and that is deliberate. All three cases (no such id, an id belonging to another key, a removed record) return the same
404. A distinct answer for someone else's operation would confirm that the id exists, and the id is itself the access key to the record: guessing one would let you read someone else's deploy. - 410 differs from 404 only for the owner. "The outcome is no longer stored" appears when the operation is yours and definitely ran, more than 7 days ago. An id that belongs to someone else, or a made-up one, never receives that answer.
- The record outlives the outcome. After 7 days the outcome stops being served, but the fact that the operation existed is kept for a while longer — that is what makes the distinguishable
410possible at all. The record is removed afterwards, and the same id starts answering404. unknownis an answer, not an error. The operation definitely ran, but a transport or process loss means the platform does not know how it ended. Reconcile the current server state and logs first; redeploy only after that check.- The polling limit is 60 requests per minute per key. The exact value arrives in the
x-ratelimit-limitheader (the cap is divided across replicas). An outcome does not change faster than the deploy itself, so polling every few seconds is enough. - Read an operation started by another key through the server. This endpoint answers only the creator key. The server owner under another of their keys, and a development team member with the settings permission, read the same outcome through
GET /v1/infra/servers/:id/operations/:operationId.