Cron, sessions & attachments API
Cron job CRUD and run history, chat session management, and file attachment uploads.
This page covers three related areas of the Gateway REST API: scheduling jobs under /api/cron, managing dashboard chat sessions under /api/sessions, and uploading file attachments for those sessions. Use these endpoints when you build your own dashboard, automate scheduled agent work over HTTP, or wire a custom chat client into the gateway.
All routes below require a bearer token from the pairing flow:
Authorization: Bearer <token>The gateway applies a 64 KiB body cap and a 30 s timeout to standard JSON routes (override with the REVKA_GATEWAY_TIMEOUT_SECS env var); the attachment upload route raises the body limit to 25 MiB. For the API as a whole, see the Gateway API overview.
Cron jobs
Section titled “Cron jobs”A cron job has one of two execution backends over the HTTP API:
shell— runs a shell command (validated against the security policy).agent— runs an LLM prompt, optionally delivering the output to a channel.
(A third internal type, workflow, is managed by YAML triggers and cannot be created through this API. See Declarative jobs & scheduler config.)
List jobs
Section titled “List jobs”GET /api/cronReturns every registered job, including its last run metadata and last output (capped at 16 KiB on disk).
{ "jobs": [ { "id": "…", "job_type": "agent", "schedule": { … }, … } ] }Create a job
Section titled “Create a job”POST /api/cronContent-Type: application/json{ "schedule": "0 9 * * *", "job_type": "agent", "prompt": "Summarize overnight alerts and post to the team channel", "name": "Morning summary", "model": null, "delivery": { "mode": "announce", "channel": "slack", "to": "#ops-alerts", "best_effort": true }, "session_target": "isolated", "allowed_tools": null, "delete_after_run": false}| Field | Type | Required | Meaning |
|---|---|---|---|
schedule | string | yes | Cron expression, e.g. "0 9 * * *" (string only — see below) |
job_type | string | no | "shell" or "agent"; inferred as "agent" when prompt is present |
command | string | for shell | Shell command to run |
prompt | string | for agent | Prompt to run as an agent turn |
name | string | no | Human-readable label |
model | string | no | Model override for agent jobs (any provider/model string) |
delivery | object | no | Channel delivery config (see below) |
session_target | string | no | "isolated" (default) or "main" for agent jobs |
allowed_tools | string[] | no | Restrict tools available to an agent job; omit/null for all tools |
delete_after_run | bool | no | Auto-delete after one run (defaults to true only for at schedules, which this endpoint cannot create) |
Success returns the created job:
{ "status": "ok", "job": { "id": "…", … } }The delivery object supports mode ("none" or "announce"), channel (telegram, discord, slack, mattermost, signal, matrix, qq), to (the channel-specific destination — Slack channel name, Discord/Telegram chat ID, Matrix room ID, etc.), and best_effort (default true; when false, a failed delivery marks the whole run as error). Job output is scanned for credential leaks and redacted before delivery. See Agent jobs & delivery.
For session_target, only "main" (run in the shared interactive session) and "isolated" (a fresh blank context, the default) are recognized; any other value falls back to isolated.
Update a job
Section titled “Update a job”PATCH /api/cron/{id}Content-Type: application/json{ "name": "New label", "schedule": "0 8 * * *", "command": "echo updated", "enabled": true}| Field | Type | Meaning |
|---|---|---|
name | string | New label |
schedule | string | New cron expression (string only) |
command | string | New value; routed to the prompt for agent jobs |
prompt | string | Alias for command on agent jobs |
enabled | bool | Enable or disable the job |
The gateway routes the edited text to either the command (shell jobs) or the prompt (agent jobs) based on the job’s stored type. Returns { "status": "ok", "job": { … } }.
Delete a job
Section titled “Delete a job”DELETE /api/cron/{id}Permanently removes the job and cascades to its run history. Returns { "status": "ok" }.
Run history
Section titled “Run history”GET /api/cron/{id}/runs?limit=20Returns recent execution records, newest first. limit is clamped to the range 1–100 (default 20). A missing job id returns 404.
{ "runs": [ { "id": "…", "job_id": "…", "started_at": "2026-06-18T09:00:00+00:00", "finished_at": "2026-06-18T09:00:03+00:00", "status": "ok", "output": "…", "duration_ms": 3120 } ]}status is "ok" or "error". Stored output is capped at 16 KiB per run; the number of records retained per job is governed by max_run_history (see settings). For more on storage and pruning, see Agent jobs & delivery.
Cron settings
Section titled “Cron settings”Read or change the subsystem’s global flags at runtime:
GET /api/cron/settingsPATCH /api/cron/settings{ "enabled": true, "catch_up_on_startup": false, "max_run_history": 100 }| Field | Type | Meaning |
|---|---|---|
enabled | bool | Global on/off switch for the entire cron subsystem |
catch_up_on_startup | bool | Run jobs that were due while the daemon was offline, once at boot |
max_run_history | int | Records kept per job; older runs are pruned automatically |
PATCH accepts any subset of these fields, persists them to the on-disk config, and applies them to the live config immediately — no daemon restart required. The response echoes the resulting settings.
String vs. object schedules
Section titled “String vs. object schedules”The schedule format is the single most important difference between the HTTP API and the agent tools / CLI / config file:
-
Gateway API (
POST/PATCH /api/cron):scheduleis a plain cron-expression string, e.g."0 9 * * *". The gateway wraps it as a cron schedule with UTC — you cannot set a timezone, and you cannot createat(one-shot) orevery(interval) jobs through this API. -
Agent tools, CLI, and
[[cron.jobs]]config:scheduleis a typed object with akinddiscriminator:{ "kind": "cron", "expr": "0 9 * * *", "tz": "America/New_York" }{ "kind": "every", "every_ms": 3600000 }{ "kind": "at", "at": "2026-12-31T23:59:00Z" }
If you need a timezone, an interval, or a one-shot schedule, create the job with the revka cron CLI (add --tz, add-every, add-at, once) or the cron_add agent tool; you can still list, inspect, and delete those jobs over HTTP afterward. Cron expressions accept standard 5-field crontab syntax (Revka uses 1 = Monday); see Cron overview & expressions for the normalization and weekday rules.
Chat sessions
Section titled “Chat sessions”These endpoints manage the persisted WebSocket chat sessions that back the dashboard’s Operator chat. They are available only when session persistence is enabled (see Session TTL & persistence); when it is off, list endpoints return an empty set and item endpoints return 404.
Dashboard sessions are stored under the gw_ key prefix; the API exposes them by their bare id. The list endpoint also surfaces channel sessions (Telegram, Discord, Slack, and so on) when channel session persistence is enabled. For the conceptual model, see Sessions & conversation state.
List sessions
Section titled “List sessions”GET /api/sessions{ "sessions": [ { "id": "…", "channel": "dashboard", "name": "My session", "started_at": "2026-06-18T08:00:00+00:00", "last_activity": "2026-06-18T09:12:00+00:00", "status": "active", "message_count": 14 } ], "archived_session_ids": ["…"]}status is derived from recency: "active" if the last activity was under 5 minutes ago, otherwise "idle". name is present only if the session has been renamed. archived_session_ids lists sessions that were soft-deleted (see Delete).
List running sessions
Section titled “List running sessions”GET /api/sessions/runningReturns only sessions that currently have an in-flight turn:
{ "sessions": [ { "session_id": "…", "created_at": "…", "last_activity": "…", "message_count": 14 } ] }Load messages
Section titled “Load messages”GET /api/sessions/{id}/messagesReturns the persisted transcript:
{ "session_id": "…", "messages": [ { "role": "user", "content": "…" }, { "role": "assistant", "content": "…" } ], "session_persistence": true}When persistence is disabled the response still returns 200 with an empty messages array and "session_persistence": false, so clients degrade gracefully.
Get session state
Section titled “Get session state”GET /api/sessions/{id}/state{ "session_id": "…", "state": "running", "turn_id": "…", "turn_started_at": "2026-06-18T09:12:00+00:00" }state reflects the live execution status (for example idle or running). turn_id and turn_started_at are present only while a turn is in flight. An unknown id returns 404.
Rename a session
Section titled “Rename a session”PUT /api/sessions/{id}Content-Type: application/json
{ "name": "Quarterly planning" }name is required and must be non-empty (otherwise 400). An unknown id returns 404. Success returns { "session_id": "…", "name": "Quarterly planning" }.
Delete a session
Section titled “Delete a session”DELETE /api/sessions/{id}This archives (soft-deletes) the session rather than erasing it, so the history remains recoverable and the id appears under archived_session_ids in the list response.
{ "deleted": true, "archived": true, "archived_at": "2026-06-18T09:30:00+00:00", "session_id": "…" }Session TTL & persistence
Section titled “Session TTL & persistence”Session storage is controlled by gateway config:
| Config key | Type | Meaning |
|---|---|---|
[gateway] session_persistence | bool | When true, dashboard chat sessions are persisted to a SQLite backend. When false, the session endpoints have nothing to manage. |
[gateway] session_ttl_hours | int | How long a session is retained before it is eligible for cleanup. |
Channel sessions appear in GET /api/sessions only when channel session persistence is also enabled. See TLS, rate limiting, WebAuthn & static serving for related gateway settings and Sessions & conversation state for how state is kept across turns.
Chat attachment upload
Section titled “Chat attachment upload”Attachments let a chat turn include an image or document. The flow is two steps: upload the file to get a file_id, then reference that id in your next WebSocket message.
Upload a file
Section titled “Upload a file”POST /api/sessions/{session_id}/attachmentsContent-Type: multipart/form-dataSend exactly one file field named file. The maximum size is 25 MiB per file; empty files and a missing file field both return 400, and an over-limit file returns 413.
curl -X POST "https://<gateway>/api/sessions/<session_id>/attachments" \ -H "Authorization: Bearer <token>" \On success the server stores the file under <workspace>/attachments/<session_id>/ and returns 201:
{ "file_id": "…", "filename": "report.pdf", "size": 184320, "mime": "application/pdf", "session_id": "…", "created_at": "2026-06-18T09:40:00+00:00"}The file_id flow
Section titled “The file_id flow”-
Upload the file with the request above and keep the returned
file_id. -
Reference it in your next chat message over the WebSocket by including the id in the
attachmentsarray:{ "type": "message", "content": "Summarize this report", "attachments": ["<file_id>"] } -
The gateway resolves each
file_idagainst the samesession_idand rewrites it into a marker before the agent turn:- MIME types beginning with
image/become an[IMAGE:…]marker, which rides the existing vision pipeline so vision-capable models see it as a content block. - Everything else becomes a
[DOCUMENT:…]marker (text is inlined for the model).
- MIME types beginning with
For the message protocol, streaming events, and how to pass a bearer token from a browser WebSocket, see Realtime: WebSocket, SSE & Live Canvas. To use chat from the dashboard UI instead, see Run the dashboard and Chat with your agent.