Agents

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:

  1. The agent config is resolved by agent_id
  2. runAgentLoop() is invoked with a synthetic user ID (scheduled:<row-id>) and the input_message
  3. On success, last_run_at is updated; on failure, last_run_error is set
The gateway must be restarted (or loadDynamicScheduledAgents() called again) to pick up rows inserted after startup. A hot-reload endpoint for this is planned.

Table schema

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

sql
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:

ExpressionSchedule
0 8 * * 1-58am, weekdays only
*/15 * * * *Every 15 minutes
0 9,17 * * *9am and 5pm daily
0 0 * * 0Midnight 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:

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

sql
-- 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 AgentsHeartbeat Daemon
ConfigurationDatabase rowsastra.yml
Trigger conditionTime only (cron)Time + data condition (RSS, email, price)
Hot-reloadRequires restartReads from file on each tick
Use caseRegular reports, syncs, digestsEvent-driven reactions