
## 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`](./deploy.md) and a command [`POST /exec`](./exec.md). 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 as `running` in the [server operation list](./operations.md) 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](./operations.md).

For a command [`POST /exec`](./exec.md), the record is opened after the server lock is acquired and before dispatch to the agent. With durable `async=true`, only `STANDALONE` returns the id in the immediate `202` header and body after the bounded launch acknowledgement wait; the command continues independently. Synchronous standalone exec sends the header before waiting for completion. Synchronous Galaxy exec sends it only with the terminal response: an interrupted wait may receive no id, so find the run through the [server operation list](./operations.md) with `?kind=exec`.

Where the id comes from — three channels, all carried by the same run:

- the `X-Vibe-Operation-Id` response 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 synchronous Galaxy exec and Galaxy deploy; durable `async=true` exec returns it with `202` on `STANDALONE` only;
- the first `event: operation` frame with `?stream=true` — for synchronous exec and for a deploy on a standalone machine, but only after any blocking wake; durable exec has no SSE; a browser `EventSource` does not expose response headers, so this is its only pre-pipeline channel;
- the `operationId` field in the body — `data.operationId` on success, `error.operationId` on 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`](./operations.md), then reconcile the server state and logs.

**Durable exec is standalone only.** [`async=true`](./exec.md) accepts a timeout from 1 to 86400 seconds on `STANDALONE`. Other server kinds receive `400 DURABLE_EXEC_UNSUPPORTED_SERVER_KIND` before locking, operation creation or command dispatch, without a synchronous fallback. Galaxy exec stays synchronous with a 600-second limit. A running standalone durable job keeps its execution channel busy after `202`; poll this operation rather than resend the command. An unknown outcome needs reconciliation and confirmed termination. `/unstick` and lock deletion do not clear durable admission.

## Parameters

| Parameter | In | Type | Required | Description |
|-----------|-----|------|:--------:|-------------|
| `operationId` | path | string | yes | Operation ID from the [`POST /deploy`](./deploy.md) or [`POST /exec`](./exec.md) response |

## Examples

### curl — personal key

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  https://vibecode.bitrix24.com/v1/infra/operations/OPERATION_ID
```

### curl — OAuth application

```bash
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

```javascript
// 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

```javascript
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 operation targets |
| `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 code of the command if confirmed by the agent, otherwise `null`. A number arrives only together with `status: "succeeded"` — see the pair rule below the status table |
| `data.stdout` | string (optional) | Retained durable exec standard output, including partial output for `unknown` when available; only for a scoped key owned by the operation creator |
| `data.stderr` | string (optional) | Retained durable exec error stream; same private scope as `stdout` |
| `data.duration` | number (optional) | Retained durable exec duration in milliseconds; same private scope as `stdout` |
| `data.truncated` | boolean (optional) | Whether retained output reached the shared 1 MiB stdout+stderr limit; same private scope as `stdout` |
| `data.startedAt` | string | Operation record creation time, ISO 8601. For durable exec it does not prove the process launched |
| `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` | No terminal operation outcome has been recorded yet. A durable command may be executing or awaiting launch confirmation, including after a lost ACK; this status does not prove a process launched. Poll the same operation instead of resending the command |
| `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. This proves only the refused attempt never ran; for `EXEC_BUSY`, check the operation occupying the channel and any earlier attempts with lost acknowledgements before retrying |
| `unknown` | The platform cannot prove the remote outcome: the transport was lost or the process died between the start and the outcome write. The operation record exists, but it proves neither that the command started nor that it stopped. 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`](/docs/infra/servers/get) and the [logs](./logs.md) 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 this attempt never started; `EXEC_BUSY` still requires reconciliation of the occupying operation before retrying; with `running` the attempt awaits completion or launch confirmation, so poll the same operation; with `unknown` the outcome is not proven.

**What is stored and what is not.** A synchronous exec record carries the outcome of the attempt — status, exit status and the reason for a refusal — without stdout/stderr. Use [`async=true`](./exec.md) on `STANDALONE` for a durable command with encrypted retained output. This endpoint returns that output only when the current scoped key belongs to the user who created the operation. A same-owner key rotation preserves access; transfer to a different owner moves operation metadata but never its private output. Rows without a recorded creator identity return metadata only. Server-scoped reads never return command output.

## Durable exec refusal codes

For `kind: "exec"` and `status: "failed"`, the command was proven not to have started. `data.error.code` can name an admission refusal already returned by POST, or a prelaunch refusal discovered after HTTP `202`. The HTTP status of this GET remains `200`; these codes describe the recorded command attempt, not a failed status read. Confirm the operation status before starting a corrected request. For `EXEC_BUSY`, read the server operation list with `kind=exec` and reconcile the occupying attempt, including lost acknowledgements. Wait for a known running operation; an unknown outcome requires state and log checks or support, rather than another exec.

| Code | Condition | Client action |
|------|-----------|---------------|
| `EXEC_JOB_STORAGE_UNAVAILABLE` | The operation or its target could not be stored | Check the operation if an id was returned; resolve storage availability before retrying |
| `EXEC_BUSY` | Execution admission was occupied | Wait for the known running job; contact support for an unknown outcome |
| `EXEC_JOB_CAPACITY` | The host already retains 64 job records | Wait for eligible retention cleanup or contact support; no automatic resend |
| `EXEC_JOB_UNAVAILABLE` | Durable admission, storage or launch is unavailable | Resolve availability with support; do not replace with a synchronous execution |
| `EXEC_JOB_INVALID` | The agent rejected durable launch data | Check the recorded refusal and request; contact support if a valid request is refused |
| `EXEC_JOB_START_WINDOW` | The server clock disagrees with the platform clock: the launch window is outside what the agent accepts | Check the time on the server and retry; contact support if it repeats |
| `EXEC_JOB_REJECTED` | Admission was refused without a specific code | Contact support |
| `UNKNOWN_TYPE` | The execution service does not support durable jobs | Contact support to update execution support; no synchronous fallback |
| `EXEC_JOB_START_EXPIRED` | The launch window expired before the user command started | A new launch is safe after confirming this failed status; contact support if it repeats |
| `EXEC_WORKDIR_DENIED` | The working directory violates the path policy | Choose an allowed directory; see the exec workdir contract |
| `EXEC_WORKDIR_NOT_FOUND` | The working directory is absent or cannot be inspected | Use an existing accessible directory |
| `EXEC_WORKDIR_NOT_DIR` | The working path is not a directory | Use a directory |
| `EXEC_FAILED` | The standalone command interpreter could not start | Check the environment and contact support before a new launch |

An expired deadline or a lost standalone process can yield `status: "unknown"`, partial retained output and `data.error: null`. This is not a prelaunch refusal and does not authorize another exec. Reconcile state and logs, confirm execution has stopped and contact support if needed. TTL, tunnel bounce, `/unstick`, `force` and lock deletion do not clear durable admission.

## Response example

Deploy in progress:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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"
    }
  }
}
```

A durable command ran to completion; retained fields are shown to its creator through the scoped key:

```json
{
  "success": true,
  "data": {
    "operationId": "clz9k2m4x0001qw8h3f7d2n5q",
    "kind": "exec",
    "serverId": "8f14e45f-ceea-467a-9c8d-2f1c5a6b7e30",
    "status": "succeeded",
    "step": null,
    "exitCode": 0,
    "stdout": "maintenance complete\n",
    "stderr": "",
    "duration": 275000,
    "truncated": false,
    "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:

```json
{
  "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:

```json
{
  "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 |

For the full list of common API errors, see [Errors](/docs/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" means an operation record belonged to your key and its outcome window expired, seven days after its terminal outcome. It does not prove that the command or deploy ran: a refused attempt can also have an operation record. 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 `410` possible at all. The record is removed afterwards, and the same id starts answering `404`.
- **`unknown` is an answer, not an error.** The operation record exists, but a transport or process loss means the platform cannot prove whether the command started or 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 current limit is returned in the `x-ratelimit-limit` header. An outcome does not change faster than the deploy itself, so polling every few seconds is enough.
- **Read through the key currently assigned to the operation.** Key rotation or transfer moves operation metadata to the replacement key. Retained command output is available only when that key belongs to the user who created the operation: a same-owner rotation preserves output access, while a transfer to another owner returns metadata only. The server owner under a key outside the operation scope, and a development team member with the settings permission, read metadata through [`GET /v1/infra/servers/:id/operations/:operationId`](./server-operation.md).

## See also

- [Deploy an application](./deploy.md)
- [Recent server operations](./operations.md)
- [Get a server](/docs/infra/servers/get)
- [Application logs](./logs.md)
- [Operation outcome from the server feed](./server-operation.md)
