For AI agents: markdown of this page — /docs-content-en/infra/wake-schedules.md documentation index — /llms.txt
Scheduled wake
Recurring wake windows for a server in BLACKHOLE mode: the platform brings the server up by a given cron-expressed moment, and the task itself is fired by the app's own cron once the machine is already up — the platform never reads or runs the app's code.
On a preemptible plan, waking is best-effort, not a guarantee. A server of this type comes up as capacity frees up — through a queue — so there may be no free capacity by the window's moment, and the window can be skipped. The create and update response carries preemptibleAdvisoryCode: "PREEMPTIBLE_BEST_EFFORT" for exactly this case — see Create a window. For time-critical tasks, use a non-preemptible plan or the always-on (24/7) mode.
Scope: vibe:infra
Create a wake window
POST /v1/infra/servers/:id/wake-schedules
Declares a new recurring wake window for a server in BLACKHOLE mode. The body is a flat object, with no fields wrapper.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string (UUID) | yes | ID of the BLACKHOLE server |
Request fields (body)
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
cronExpr |
string | yes | — | A 5-field cron expression (minute hour day-of-month month day-of-week), 9 to 120 characters |
timezone |
string | yes | — | IANA timezone of the window, e.g. America/New_York. Checked against Intl.supportedValuesOf('timeZone') — an unknown value is rejected at validation |
label |
string | no | — | Display label for the window, up to 80 characters |
lead |
number | no | the server's margin, else the platform's | Margin, in seconds, before the cronExpr moment by which the server must already be up — covers VM boot time. 0 to 3600 |
enabled |
boolean | no | true |
Whether the window is active. A disabled window doesn't participate in waking but stays in the list |
The lead margin resolves in three tiers: the window's own value, else a server-level margin set apart from this CRUD, else the platform default — currently 180 seconds. For a server with kind: "GALAXY_APP", waking runs in two phases — bringing up the host and starting the container — so budget lead up to 900 seconds.
Examples
curl — personal key
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/wake-schedules" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"cronExpr": "0 9 * * 1-5",
"timezone": "America/New_York",
"label": "daily report",
"enabled": true
}'
curl — OAuth app
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/wake-schedules" \
-H "X-Api-Key: YOUR_APP_KEY" \
-H "Authorization: Bearer USER_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"cronExpr": "0 9 * * 1-5",
"timezone": "America/New_York",
"label": "daily report",
"enabled": true
}'
JavaScript — personal key
const res = await fetch(
`https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/wake-schedules`,
{
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
cronExpr: '0 9 * * 1-5',
timezone: 'America/New_York',
label: 'daily report',
enabled: true,
}),
}
)
const body = await res.json()
if (body.tzWarningCode === 'MULTI_ZONE') {
console.warn('Windows span more than one timezone — TZ is not injected at deploy time')
}
JavaScript — OAuth app
const res = await fetch(
`https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/wake-schedules`,
{
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_APP_KEY',
'Authorization': 'Bearer USER_SESSION_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
cronExpr: '0 9 * * 1-5',
timezone: 'America/New_York',
label: 'daily report',
enabled: true,
}),
}
)
Response fields
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true on success |
data.id |
string | Window ID for a later update or delete |
data.serverId |
string (UUID) | ID of the owning server |
data.cronExpr |
string | The stored cron expression |
data.timezone |
string | The stored timezone |
data.label |
string | null | Window label |
data.lead |
number | null | Margin in seconds. null when not set on the window itself |
data.enabled |
boolean | Whether the window is active |
data.source |
string | How the window was created. Always "MANUAL" for windows created through this CRUD |
data.lastFiredAt |
string (ISO 8601) | null | Timestamp of the last confirmed firing. null for a freshly created window |
data.wakeAttemptStartedAt |
string (ISO 8601) | null | Internal scheduler claim marker — non-null only during the short window while a wake is in flight. Normally null |
data.lastWakeLateAt |
string (ISO 8601) | null | Timestamp of the last wake that fired later than its computed window. null if there have been no late wakes |
data.createdAt |
string (ISO 8601) | Record creation date |
data.updatedAt |
string (ISO 8601) | Date of the last update to the record |
tzWarning |
string | null | Timezone-mismatch advisory, as text. null when the server has no active windows left after this mutation |
tzWarningCode |
string | null | Machine-readable code for the same advisory: "SINGLE_ZONE", "MULTI_ZONE", or null |
preemptibleAdvisoryCode |
string | null | "PREEMPTIBLE_BEST_EFFORT" when the server runs on a preemptible plan — otherwise null |
preemptibleAdvisory |
string | null | An explanation of the same advisory, as text (in English, for non-UI API clients). null when the server is on a non-preemptible plan |
tzWarning, tzWarningCode, preemptibleAdvisoryCode, and preemptibleAdvisory are top-level response fields, not nested under data.
Response example
The server's only active window — the timezone is unambiguous:
{
"success": true,
"data": {
"id": "cm38x02qp0001ml08g7k3h2a",
"serverId": "e765edfc-ba0a-43de-b8ea-838dd872c522",
"cronExpr": "0 9 * * 1-5",
"timezone": "America/New_York",
"label": "daily report",
"lead": null,
"enabled": true,
"source": "MANUAL",
"lastFiredAt": null,
"wakeAttemptStartedAt": null,
"lastWakeLateAt": null,
"createdAt": "2026-07-10T08:12:00.000Z",
"updatedAt": "2026-07-10T08:12:00.000Z"
},
"tzWarning": "The VM may have been deployed with a different timezone (or not redeployed since this window was declared) — verify the in-VM cron or redeploy the app so TZ is re-injected.",
"tzWarningCode": "SINGLE_ZONE",
"preemptibleAdvisoryCode": null,
"preemptibleAdvisory": null
}
A server on a preemptible plan would get "preemptibleAdvisoryCode": "PREEMPTIBLE_BEST_EFFORT" and a non-empty preemptibleAdvisory instead of null in those same two fields — the rest of the response shape is unchanged.
Error response example
400 — the window conflicts with an always-on server:
{
"success": false,
"error": {
"code": "ALWAYS_ON_CONFLICT",
"message": "Scheduled wake conflicts with an always-on (24/7) server, which never auto-sleeps. Turn off the always-on toggle, or set a sleep timeout instead of \"Never\", to schedule wake windows."
}
}
Errors
| HTTP | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR |
Body validation failed — missing cronExpr/timezone, timezone not in the IANA timezone list, label over 80 characters, lead outside 0–3600, or an unknown field in the body |
| 400 | BLACKHOLE_ONLY |
The server is not in BLACKHOLE mode |
| 400 | GALAXY_NOT_SUPPORTED |
The server is a Galaxy host (kind: "GALAXY"). Nested apps (kind: "GALAXY_APP") are accepted — only the host itself is rejected |
| 400 | ALWAYS_ON_CONFLICT |
The server runs in always-on mode — no auto-sleep. A wake schedule would silently break that guarantee |
| 400 | CADENCE_TOO_LOW |
The gap between consecutive cronExpr occurrences is below the platform's floor — currently 5 minutes |
| 401 | MISSING_API_KEY |
The X-Api-Key header is missing |
| 401 | INVALID_API_KEY |
The API key is invalid or expired |
| 403 | WAKE_SCHEDULE_DISABLED |
Scheduled wake is not enabled for this portal |
| 403 | WAKE_SCHEDULE_LIMIT |
The server already has 50 windows — the per-server cap |
| 403 | INFRA_SCOPE_REQUIRED |
The key lacks the vibe:infra scope |
| 404 | NOT_FOUND |
The server was not found, was deleted, or belongs to a different API key |
| 429 | RATE_LIMITED |
The 10-requests-per-minute limit for this endpoint was exceeded |
Full list of shared error codes — Errors.
Known behavior
- Check order. The key's scope and server ownership are checked before the other gates, so
WAKE_SCHEDULE_DISABLED/BLACKHOLE_ONLY/GALAXY_NOT_SUPPORTED/ALWAYS_ON_CONFLICTonly ever surface for a request with a valid key against an owned, existing server. The cron-expression checks (VALIDATION_ERROR, thenCADENCE_TOO_LOW) run after these gates. tzWarning/tzWarningCodeare a conservative heuristic. The platform doesn't track whether the server was actually redeployed since a window was declared, so the advisory fires for any set of active windows after a mutation — even when the timezone is already correctly set in the environment. Go by the code:"SINGLE_ZONE"means the timezone is unambiguous and lands inTZon the next deploy,"MULTI_ZONE"means the windows span more than one zone andTZis not auto-injected.preemptibleAdvisoryCode/preemptibleAdvisoryare an advisory, not a gate. The window is created or updated regardless of these fields' value — on a preemptible plan the platform does not forbid a schedule (that's its intended use case), it only honestly reports that waking by the window's moment is best-effort. Localize on thepreemptibleAdvisoryCodecode, not on thepreemptibleAdvisorystring — that is a fixed English string for non-UI clients, not meant to be shown to a user as-is.- The platform does not configure the cron inside the VM.
POST /wake-schedulesonly guarantees the server is up by the target moment (best-effort on a preemptible plan — seepreemptibleAdvisoryCodeabove). Firing the task at that moment is the app's owncron/timer's job.
See also
- List windows
- Update a window
- Delete a window
- What the app receives — deploy timezone and the in-VM cron