Skip to content

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.

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.)

GET /api/cron

Returns every registered job, including its last run metadata and last output (capped at 16 KiB on disk).

{ "jobs": [ { "id": "…", "job_type": "agent", "schedule": { }, } ] }
POST /api/cron
Content-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
}
FieldTypeRequiredMeaning
schedulestringyesCron expression, e.g. "0 9 * * *" (string only — see below)
job_typestringno"shell" or "agent"; inferred as "agent" when prompt is present
commandstringfor shellShell command to run
promptstringfor agentPrompt to run as an agent turn
namestringnoHuman-readable label
modelstringnoModel override for agent jobs (any provider/model string)
deliveryobjectnoChannel delivery config (see below)
session_targetstringno"isolated" (default) or "main" for agent jobs
allowed_toolsstring[]noRestrict tools available to an agent job; omit/null for all tools
delete_after_runboolnoAuto-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.

PATCH /api/cron/{id}
Content-Type: application/json
{
"name": "New label",
"schedule": "0 8 * * *",
"command": "echo updated",
"enabled": true
}
FieldTypeMeaning
namestringNew label
schedulestringNew cron expression (string only)
commandstringNew value; routed to the prompt for agent jobs
promptstringAlias for command on agent jobs
enabledboolEnable 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 /api/cron/{id}

Permanently removes the job and cascades to its run history. Returns { "status": "ok" }.

GET /api/cron/{id}/runs?limit=20

Returns 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.

Read or change the subsystem’s global flags at runtime:

GET /api/cron/settings
PATCH /api/cron/settings
{ "enabled": true, "catch_up_on_startup": false, "max_run_history": 100 }
FieldTypeMeaning
enabledboolGlobal on/off switch for the entire cron subsystem
catch_up_on_startupboolRun jobs that were due while the daemon was offline, once at boot
max_run_historyintRecords 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.

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): schedule is 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 create at (one-shot) or every (interval) jobs through this API.

  • Agent tools, CLI, and [[cron.jobs]] config: schedule is a typed object with a kind discriminator:

    { "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.

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.

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).

GET /api/sessions/running

Returns only sessions that currently have an in-flight turn:

{ "sessions": [ { "session_id": "…", "created_at": "…", "last_activity": "…", "message_count": 14 } ] }
GET /api/sessions/{id}/messages

Returns 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 /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.

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 /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 storage is controlled by gateway config:

Config keyTypeMeaning
[gateway] session_persistenceboolWhen true, dashboard chat sessions are persisted to a SQLite backend. When false, the session endpoints have nothing to manage.
[gateway] session_ttl_hoursintHow 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.

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.

POST /api/sessions/{session_id}/attachments
Content-Type: multipart/form-data

Send 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.

Terminal window
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"
}
  1. Upload the file with the request above and keep the returned file_id.

  2. Reference it in your next chat message over the WebSocket by including the id in the attachments array:

    { "type": "message", "content": "Summarize this report", "attachments": ["<file_id>"] }
  3. The gateway resolves each file_id against the same session_id and 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).

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.