arizuko › components › timed
timed is the scheduler daemon. One Go binary that ticks every 60 seconds, claims due rows from the scheduled_tasks table, and inserts each task’s prompt into messages as if a user had just typed it.
Once the message lands in the table, gated picks it up through the normal poll loop and routes it to the task’s folder. The agent has no idea the prompt came from a clock.
Cron, recurring runs, and one-shot reminders all collapse to one need: drop a prompt into a folder at a future time. Keeping that loop inside gated would mix two clocks — one for inbound polling, one for scheduling — and tie cron behavior to gateway restarts.
A separate daemon writing to the same DB means gated stays a pure router. Without timed, no task ever fires. Cron tools (schedule_task, pause_task, …) still write rows, but nothing claims them.
Each 60-second tick:
UPDATE flips every row where status='active' AND next_run ≤ now to status='firing'. Two timed instances cannot double-fire the same task.messages with sender='timed' (or timed-isolated:<id> when the task asked for an isolated session) and the task’s chat_jid.cron column as a 5-field cron expression in the daemon’s TZ, or as a millisecond interval, or leave empty for one-shots.next_run back, flip status to active (or completed for a fired one-shot), and append one row to task_run_logs.agent (or operator)
| schedule_task / pause_task / …
v
scheduled_tasks (rows with cron + next_run)
|
| timed ticks every 60s, claims due rows
v
messages (one row per fire, sender='timed')
|
v
gated ---> routes by chat_jid ---> agent run
Inputs: scheduled_tasks rows written by MCP tools through gated. Outputs: synthetic messages rows and audit rows in task_run_logs.
Hard deps: read/write access to the same messages.db gated uses. No HTTP API today beyond /health; the /v1/tasks surface is planned (see timed/README.md).
Concepts: concepts/tasks covers the row anatomy, context modes, and the five MCP tools agents use to manage tasks.
Yes. timed is a plain Go binary that needs an SQLite file with the scheduled_tasks and task_run_logs tables already migrated — gated owns the schema, so spin up gated first (or copy its DB).
# build
make build # produces ./timed
# run against an existing arizuko data dir
DATA_DIR=/srv/data/myinstance TZ=Europe/Prague ./timed
# or point at any sqlite DSN directly
DATABASE=/path/to/messages.db ./timed
GET /health on :8080 returns 200 when db.Ping() succeeds. Red flag: no rows appearing in task_run_logs while scheduled_tasks has active rows past their next_run.
DATA_DIR — resolves <DATA_DIR>/store/messages.db when DATABASE is unset.DATABASE — explicit SQLite DSN; wins over DATA_DIR.TZ — timezone used to evaluate cron expressions (default UTC).Full list and defaults in reference/env.
timed/README.md — tables owned, planned /v1/tasks surface, token contract.specs/4/8-scheduler-service.md — original design.