## Run write SQL

`POST /v1/infra/servers/:id/db/execute`

Runs write SQL statements against the application's SQLite database in one transaction: changes rows, creates and alters tables, indexes and views.

Before each call, the platform saves a separate protective backup of the database — see [Protective backups before writes](/docs/infra/database#protective-backups-before-writes). Access conditions — [When the database is available](/docs/infra/database#when-the-database-is-available).

## Parameters

| Parameter | Type | Required | Description |
|----------|-----|:-----:|----------|
| `id` (path) | string (UUID) | yes | ID of an application with `kind` equal to `GALAXY_APP`. List: [`GET /v1/infra/servers`](/docs/infra/servers/list) |

## Request fields (body)

| Field | Type | Required | Description |
|------|-----|:-----:|----------|
| `file` | string | no | Database file path relative to the `/data` volume, up to 1024 bytes in UTF-8. Absolute paths and `.` or `..` segments are not accepted. Defaults to the `VIBE_DB_PATH` value of the application container, or `app.db` if it is not set. File list: [`GET /db`](/docs/infra/database/info) |
| `sql` | string | yes | From 1 to 16384 bytes in UTF-8. Up to 64 statements separated by `;`. Statements run in order |
| `parameters` | array | no | Values for `?` placeholders, up to 256 items. An item is `null`, a string up to 16384 bytes in UTF-8 or a finite number. An integer must not exceed 2⁵³−1 in absolute value; pass an exact 64-bit integer as a literal in `sql`. Booleans are not accepted — pass `1` or `0`. Allowed only when `sql` contains one statement. Default: `[]` |

An unknown body field is rejected with `400 VALIDATION_ERROR`.

The following in `sql` are rejected with `422 APP_DB_SQL_ERROR` and reason `not_permitted`: transaction and savepoint control (`BEGIN`, `COMMIT`, `ROLLBACK`, `SAVEPOINT`), `ATTACH` and `DETACH`, `PRAGMA`, creating and dropping virtual tables, access to schemas other than `main`, including temporary tables, and the `load_extension`, `readfile`, `writefile` and `fts3_tokenizer` functions. `VACUUM` does not run either: the call runs inside a transaction, and the statement is rejected with `422 APP_DB_SQL_ERROR`.

## Examples

### curl — personal key

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/APP_ID/db/execute" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sql":"UPDATE notes SET body = ? WHERE id = ?","parameters":["updated",1]}'
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/APP_ID/db/execute" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sql":"UPDATE notes SET body = ? WHERE id = ?","parameters":["updated",1]}'
```

### JavaScript — personal key

```javascript
const url = `https://vibecode.bitrix24.com/v1/infra/servers/${appId}/db/execute`
const headers = { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' }
const body = JSON.stringify({ sql: 'UPDATE notes SET body = ? WHERE id = ?', parameters: ['updated', 1] })

// Only 409 APP_DB_BACKUP_NOT_READY is retried: with this response the SQL has not run yet
const deadline = Date.now() + 180_000
let result
while (Date.now() < deadline) {
  const res = await fetch(url, { method: 'POST', headers, body })
  result = await res.json()
  if (res.status !== 409 || result.error?.code !== 'APP_DB_BACKUP_NOT_READY') break
  const seconds = Number(res.headers.get('Retry-After')) || 1
  await new Promise(resolve => setTimeout(resolve, Math.max(seconds, 3) * 1000))
}
if (!result.success) throw new Error(result.error.code)
console.log(result.data.rowCount, result.data.backupId)
```

### JavaScript — OAuth application

```javascript
const url = `https://vibecode.bitrix24.com/v1/infra/servers/${appId}/db/execute`
const headers = {
  'X-Api-Key': 'YOUR_APP_KEY',
  'Authorization': 'Bearer USER_SESSION_TOKEN',
  'Content-Type': 'application/json',
}
const body = JSON.stringify({ sql: 'UPDATE notes SET body = ? WHERE id = ?', parameters: ['updated', 1] })

const deadline = Date.now() + 180_000
let result
while (Date.now() < deadline) {
  const res = await fetch(url, { method: 'POST', headers, body })
  result = await res.json()
  if (res.status !== 409 || result.error?.code !== 'APP_DB_BACKUP_NOT_READY') break
  const seconds = Number(res.headers.get('Retry-After')) || 1
  await new Promise(resolve => setTimeout(resolve, Math.max(seconds, 3) * 1000))
}
if (!result.success) throw new Error(result.error.code)
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | `true` if the transaction was committed |
| `data.file` | string | Database file relative to the `/data` volume that the SQL was applied to |
| `data.rowCount` | integer | Number of rows changed directly by `INSERT`, `UPDATE` and `DELETE` statements. Changes made by triggers are not counted. A statement that changes the database schema, for example `CREATE TABLE … AS SELECT`, is not counted |
| `data.statementCount` | integer | Number of statements run, from 1 to 64 |
| `data.durationMs` | number | SQL execution time on the host in milliseconds, with a fractional part |
| `data.backupId` | string | ID of the protective backup saved before this call. Backup list: [`GET /db/backups`](/docs/infra/database/backups) |

## Response example

```json
{
  "success": true,
  "data": {
    "file": "app.db",
    "rowCount": 1,
    "durationMs": 0.933,
    "statementCount": 1,
    "backupId": "d5e1b7a9-0f42-4c8d-9a3e-6b21c4f80e57"
  }
}
```

## Error response example

422 — the `PRAGMA journal_mode=DELETE` statement is not permitted; the transaction was rolled back:

```json
{
  "success": false,
  "error": {
    "code": "APP_DB_SQL_ERROR",
    "message": "SQL operation is not permitted",
    "details": {
      "reason": "not_permitted"
    }
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `VALIDATION_ERROR` | The body failed validation: an unknown field, `sql` empty or longer than 16384 bytes, an invalid `file`, more than 256 items or a boolean in `parameters` |
| 400 | `APP_DB_INVALID_FILE` | The file is not an SQLite database, its path goes through a symbolic link, or the container's `VIBE_DB_PATH` value does not point inside `/data` |
| 400 | `APP_DB_UNSUPPORTED_PLACEMENT` | The application is not hosted in Galaxy |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header is missing |
| 401 | `INVALID_API_KEY` | The key is not recognized |
| 402 | `BILLING_FROZEN` | The portal's billing is frozen |
| 402 | `BILLING_INSUFFICIENT` | The portal balance is insufficient to store the protective backup |
| 403 | `INFRA_SCOPE_REQUIRED` | The key lacks the `vibe:infra` scope |
| 403 | `APP_DB_DISABLED` | Application database access is not enabled for the portal |
| 403 | `APP_DB_READONLY_KEY` | The key is read-only |
| 404 | `NOT_FOUND` | The application was not found, was deleted or is not accessible to this key |
| 404 | `APP_DB_NOT_FOUND` | The application has no `/data` volume or database file: the application has not created it yet |
| 409 | `APP_DB_BACKUP_NOT_READY` | The protective backup is still being prepared; the SQL did not run. Retry the request after the pause from the `Retry-After` header |
| 409 | `APP_DB_BUSY` | A deploy, migration or restore of the application is in progress, the host is busy with another operation, the database is busy with a write by the application itself, or waking the application or galaxy host is blocked. The SQL did not run. Retry after the pause from `Retry-After` |
| 409 | `APP_DB_WAL_CLOSED` | The protective backup was not created: a running application has closed its database connection in write-ahead logging (WAL) mode. The SQL was not sent; there is no `Retry-After` header. Configure the application to keep the connection open |
| 413 | `PAYLOAD_TOO_LARGE` | The body exceeds 64 KiB, or the full input of the service process on the host exceeds 96 KiB |
| 422 | `APP_DB_SQL_ERROR` | The SQL did not run; the transaction was rolled back. The cause is in `details.reason`: `syntax_error`, `not_permitted`, `no_such_table`, `no_such_column`, `type_mismatch`, `constraint_failed` or `other`. Reason `other` is also returned for parameters with several statements and for more than 64 statements |
| 422 | `APP_DB_QUERY_TIMEOUT` | Execution took longer than 30 seconds; the transaction was rolled back |
| 422 | `APP_DB_CORRUPT` | The database integrity check before the write failed, or the protective backup was not created because the database is corrupted |
| 422 | `APP_DB_BACKUP_TIMEOUT` | The protective backup was not created within the allotted time; the SQL was not sent |
| 422 | `APP_DB_BACKUP_INVALID` | The protective backup failed validation; the SQL was not sent |
| 429 | `RATE_LIMITED` | The write request limit for the portal was exceeded — [Limits](/docs/infra/database#limits). The pause is in the `Retry-After` header |
| 503 | `APP_DB_PRE_CHANGE_FAILED` | The protective backup was not created for another reason; the SQL was not sent |
| 503 | `APP_DB_WRITE_OUTCOME_UNKNOWN` | The SQL was sent, but no result was received; `details.retryable` is `false`. The change may have been saved. There is no idempotency key, so do not retry the request automatically: first read the data through [`POST /db/query`](/docs/infra/database/query) |
| 503 | `APP_DB_TOOL_UNAVAILABLE` | The host is not prepared or the service channel to it is unavailable. Host readiness — [`GET /app-db/prepare`](/docs/infra/database/prepare-status) |
| 503 | `user_self_deletion_pending` | The key owner is deleting their account on the portal |

Full list of common API errors — [Errors](/docs/errors).

## Known specifics

- **The first call only prepares the protective backup.** While there is no fresh backup for this file, the call queues its creation and returns `409 APP_DB_BACKUP_NOT_READY` with `details.backupId`, `details.retryAfterSeconds` and a `Retry-After` header. Repeat the same request: once the backup is ready, the retry runs the SQL and returns the same ID in `data.backupId`. A ready backup waits 10 minutes for the retry; after that, the next call prepares a new one. How protective backups work — [Protective backups before writes](/docs/infra/database#protective-backups-before-writes).
- **Retries consume the write limit.** Each retry after `409` counts toward the write request limit ([Limits](/docs/infra/database#limits)), so pause for at least three seconds, even when `Retry-After` is `1`.
- **A backup failure is reported once.** If the protective backup was not created, the next call receives the cause with `details.retryable: false`, and the call after that queues a new backup.
- **The whole call is one transaction.** An error in any statement or exceeding 30 seconds rolls back all statements of the call. Rows returned by `SELECT` and `RETURNING` are read to the end and discarded: they are not included in the response. SQL and parameter values are not written to the audit log.
- **Foreign keys are always enforced.** For the duration of the call, the platform enables foreign key checks, even if the application itself does not. A referential integrity violation rolls back the call with reason `constraint_failed`.
- **The application container is not started.** The SQL is run by a service container on the host with the same `/data` volume.
- **The AI agent tool retries only while the backup is not ready.** The `manage_app_db` tool of the Vibecode MCP server with `action: "execute"` accepts `serverId`, `file`, `sql`, `parameters` and retries only `409 APP_DB_BACKUP_NOT_READY` on its own. The total wait is 180 seconds; the pause follows `Retry-After`, but is no shorter than three seconds. The tool does not retry a network error or `APP_DB_WRITE_OUTCOME_UNKNOWN`. Cancelling the wait does not prove that an already sent write was rolled back.

## See also

- [Run a read-only SQL query](/docs/infra/database/query)
- [List database backups](/docs/infra/database/backups)
- [Restore the database from a backup](/docs/infra/database/restore)
- [Get host preparation status](/docs/infra/database/prepare-status)
- [Application database](/docs/infra/database)
