Scheduled Agents
Scheduled agents let you trigger agent runs on a cron schedule without a code deploy. Schedules are stored in the scheduled_agents table and loaded at gateway startup — adding a row is all it takes to register a new recurring job.
How it works
At startup, startScheduler() calls loadDynamicScheduledAgents(), which reads all rows where enabled = true from the scheduled_agents table and registers each as a live in-process cron job via node-cron. At each scheduled tick:
- The agent config is resolved by
agent_id runAgentLoop()is invoked with a synthetic user ID (scheduled:<row-id>) and theinput_message- On success,
last_run_atis updated; on failure,last_run_erroris set
loadDynamicScheduledAgents() called again) to pick up rows inserted after startup. A hot-reload endpoint for this is planned.Table schema
scheduled_agents (
id UUID PRIMARY KEY,
workspace_id TEXT, -- optional workspace scope
agent_id TEXT NOT NULL, -- must exist in astra.yml or DB
cron_expression TEXT NOT NULL, -- standard 5-field cron
input_message TEXT NOT NULL, -- the prompt sent to the agent
surface TEXT DEFAULT 'chat', -- routing surface
surface_id TEXT DEFAULT 'scheduled',
enabled BOOLEAN DEFAULT true,
last_run_at TIMESTAMPTZ, -- updated after every execution
last_run_error TEXT, -- last error message if run failed
created_by TEXT NOT NULL,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
)Creating a scheduled agent
Insert a row directly into the database. The agent_id must match an agent defined in astra.yml or the agents table.
INSERT INTO scheduled_agents
(workspace_id, agent_id, cron_expression, input_message, surface, surface_id, created_by)
VALUES
('ws_abc123', 'briefing-agent', '0 8 * * 1-5',
'Prepare the morning briefing: top news, open PRs, and calendar summary.',
'chat', 'scheduled', 'uid_alice');Cron syntax
Standard 5-field cron expressions are supported:
| Expression | Schedule |
|---|---|
0 8 * * 1-5 | 8am, weekdays only |
*/15 * * * * | Every 15 minutes |
0 9,17 * * * | 9am and 5pm daily |
0 0 * * 0 | Midnight every Sunday |
0 4 1 * * | 4am on the 1st of each month |
Invalid expressions are rejected at startup with an error log; the row is skipped without crashing the scheduler.
Surface and surface ID
The surface and surface_id columns control session routing — the same fields used in WebSocket and REST chat requests. Use surface = 'chat' and any stable surface_id to keep scheduled runs in a persistent session thread, or vary surface_id per run to get a fresh session each time.
Monitoring runs
Check last_run_at and last_run_error to audit recent executions:
SELECT id, agent_id, cron_expression, last_run_at, last_run_error
FROM scheduled_agents
ORDER BY last_run_at DESC NULLS LAST;Enabling and disabling
Set enabled = false to pause a job without deleting it. The gateway must reload schedules for the change to take effect.
-- Disable a scheduled agent without deleting it
UPDATE scheduled_agents SET enabled = false WHERE id = '<uuid>';
-- Re-enable it
UPDATE scheduled_agents SET enabled = true WHERE id = '<uuid>';Scheduled agents vs. Heartbeat daemon
| Scheduled Agents | Heartbeat Daemon | |
|---|---|---|
| Configuration | Database rows | astra.yml |
| Trigger condition | Time only (cron) | Time + data condition (RSS, email, price) |
| Hot-reload | Requires restart | Reads from file on each tick |
| Use case | Regular reports, syncs, digests | Event-driven reactions |