scheduled agent runs · ← concepts
A task is a saved prompt with a schedule. When the schedule says it’s time, the prompt lands in a folder as if a user had just typed it, and the agent answers like any other turn.
One row in the scheduled_tasks table = one recurring or one-shot agent run. The body is a prompt string; the trigger is either a cron expression, an interval in milliseconds, or a single future timestamp.
The timed daemon ticks every 60 seconds. On each tick it claims every row where status='active' and next_run ≤ now, flips them to 'firing', then for each claimed row inserts the prompt into the messages table with sender timed (or timed-isolated:<id>) and the task’s chat_jid.
The gateway polls messages the same way it polls every other inbound source. It sees the new row, routes by chat_jid, and fires a turn. The agent has no idea this prompt came from a clock instead of a person — same code path.
After insert, timed writes the next next_run (parsed from the cron expression or added from the interval), or marks the row 'completed' if there is no schedule. Every fire writes one row to task_run_logs with duration and status.
The cron column carries one of three shapes:
0 9 * * 1-5 for weekday mornings). Parsed by robfig/cron in the task’s timezone (the daemon’s TZ env, default UTC).1800000 = every 30 minutes. The next next_run is just now + interval after each fire.cron. The row fires once at next_run, then flips to status='completed' and stops being polled. The row stays in the table for audit; nothing deletes it.A task carries who owns it, where it fires, what it says, when it runs, and how it sees the folder around it.
Owner — the folder that owns the task. Used by ACL: an agent can only see and edit tasks whose owner it’s authorized for.ChatJID — the chat address the synthetic message will land on. This is what the route table sees, so the same routing rules apply as for a real inbound on that JID.Prompt — the body. Plain text, exactly what the agent receives as message content.Cron — cron expression, interval ms, or empty (see above).NextRun — the next fire time. Always present except on completed one-shots.Status — active (polled), paused (skipped by the claim query), or completed (one-shot already fired).ContextMode — either group (default) or isolated.Created — insertion timestamp, useful for audit and dedup.Context mode decides what session the agent runs under. With group the synthetic message hits the folder’s main session like any other turn — the task sees the folder’s prior chat history and shares state with it. With isolated the sender becomes timed-isolated:<task-id>, which the gateway treats as its own conversation. Useful for a recurring health check that shouldn’t pollute the human chat’s context.
Five MCP tools, exposed to agents whose tier permits scheduling:
schedule_task — create a task. Dedupes on prompt + chat + schedule, so re-running the same call returns the existing task ID rather than stacking duplicates.list_tasks — return every task visible to the calling folder. Plain dump of rows.pause_task — flip status to paused. The row is preserved; timed’s claim query ignores it until it’s resumed.resume_task — flip status back to active. No effect on already-active or cancelled tasks.cancel_task — permanently delete the row. Not reversible. Use pause_task if the suspension is temporary.A companion tool, inspect_tasks, joins scheduled_tasks with task_run_logs so the agent can see when each task last fired and whether it succeeded.
There is no dedicated arizuko task CLI subcommand today. Operators reach the same rows through the agent (via arizuko chat) or directly with sqlite3 on store/messages.db.
If the synthetic message insert itself fails, timed logs an error row to task_run_logs and flips the task back to active without advancing next_run. The next tick will try again.
If the insert succeeds but the agent run downstream errors out, timed never knows — from the scheduler’s point of view the fire was successful. next_run advances normally and the task fires again at its scheduled time. There is no automatic retry of the failed turn. The error sits in the gateway’s logs and the folder’s chat history; an operator (or the agent itself, on its next run) decides what to do.
The status field never auto-flips to paused based on failures. A failing recurring task keeps firing until somebody pauses it or fixes the underlying problem.
Daemon internals and the planned /v1/tasks HTTP surface: timed/README.md. Full scheduler design: specs/4/8. How the routed JID becomes a folder: routing.