Scheduled tasks

The scheduler is just another producer. The gateway does not know the scheduler exists.

Scheduled tasks in arizuko are not a gateway feature. They are a separate daemon — timed — that polls a task table and inserts rows into messages when a cron expression fires. The gateway picks those rows up in its normal poll. The integration surface is one table write.

How the agent schedules a task

The agent calls the schedule_task MCP tool, which the gateway's IPC server handles by writing to the scheduled_tasks table:

schedule_task({
  "group_folder": "main",
  "target_jid":   "telegram:-1001234567890",
  "content":      "produce today's summary and post it",
  "cron":         "0 18 * * *"
})

The tool accepts standard 5-field cron expressions. One-shot tasks use run_at instead of cron:

schedule_task({
  "group_folder": "main",
  "target_jid":   "telegram:-1001234567890",
  "content":      "send the weekly report",
  "run_at":       "2024-12-01T09:00:00Z"
})

What timed does

timed/main.go runs a poll loop (default: every 30 seconds). On each tick:

  1. Read all rows from scheduled_tasks where enabled = true
  2. For each row: evaluate the cron expression against current time
  3. If the cron fires and last_run is not within this minute: insert a row into messages
  4. Update last_run on the task

The inserted message row looks exactly like an inbound message from a channel adapter — same columns, same format. The gateway cannot tell the difference. It processes the message normally: resolves the group from target_jid, spawns a container, runs the agent.

The integration in full

-- scheduled_tasks table (simplified)
id           INTEGER PRIMARY KEY
group_folder TEXT
target_jid   TEXT
content      TEXT
cron         TEXT        -- "0 18 * * *" or NULL
run_at       DATETIME    -- one-shot or NULL
last_run     DATETIME
enabled      BOOLEAN DEFAULT 1

-- timed inserts this when the cron fires:
INSERT INTO messages (chat_jid, sender_jid, content, origin, created_at)
VALUES (target_jid, 'scheduler', content, 'scheduled', now())

timed adds a row to messages. That is the entire scheduler-to-gateway integration.

Practical example: daily summary

A group's agent is responsible for summarizing the day's activity and posting it to a Telegram channel. The agent schedules this at setup:

schedule_task({
  "group_folder": "main",
  "target_jid":   "telegram:-1001234567890",
  "content":      "produce today's summary from the diary and post it to the group",
  "cron":         "0 18 * * *"
})

At 18:00 each day, timed inserts a message. The gateway picks it up, spawns a container for the group, and the agent runs with full access to the group's diary, facts, and memory — then sends the summary via send_message.

Multiple schedulers

Because the scheduler is just a producer on the messages table, you can run multiple timed instances — one per timezone, one with finer granularity, one for a different task type. They do not interfere. Each inserts rows; the gateway processes all rows. No coordination required.

How it fits the system

The schema is the contract. timed knows the messages schema and the scheduled_tasks schema. It knows nothing about the gateway's routing logic, container lifecycle, or MCP server. The gateway knows nothing about timed.

This is the microservice design principle at work: small processes with clear contracts. The contract is a SQLite table. Any process that can write SQLite can be a task producer — a cron job, a webhook handler, an operator dashboard button. The gateway processes them all the same way.