[schedule]. This page is about the mechanism (a loop inside the server, what it checks, what it skips); the fields and the syntax of [schedule] are in agent-defs.md, and the runs it starts are ordinary researches.
Code: internal/agent_research/scheduler.go (the scheduler, Manager.RunSchedules), internal/agent_defs/cron.go (ParseCron, Cron.Next, Cron.MinGap), agent_defs.Schedule in internal/agent_defs/def.go (Next, TopicAt), the startup check in internal/agent_defs/validate.go (checkSchedule).
In short
- There is a continuous checker: one background goroutine in the server process (
scheduler.loop), started once at startup byManager.RunSchedules(). There is no external cron and no job table. - It knows the next due time of every scheduled agent. It sleeps until the earliest one (at most 1 minute at a time, so a changed clock is noticed), then starts a research for each agent that is due.
- If the previous scheduled run of that agent is still working, the new one is skipped, not queued.
- Nothing is stored. After a restart the next time after the start is used: the times the server was down are not caught up.
Startup
internal/sys/server/server.go calls research.RunSchedules() at the end of the server setup, after the agent definitions, the knowledge base and the other settings are in place. The goroutine stops with the manager’s context (server shutdown), and Manager.Wait waits for it.
RunSchedules:
- Builds the scheduler from
registry().List(): every definition with a[schedule]that is notpausedbecomes an entry (scheduled) withnext = Schedule.Next(now): the first time after now the expression matches, read in the schedule’s time zone. Each one is logged (agent research: scheduled agent, withcron,timezone,user,next). - If there are no entries it returns 0 and starts nothing.
- If the manager has no LLM client (
ANTHROPIC_API_KEYnot set) it logs a warning and starts nothing: the agents will not run. - Otherwise it starts the loop goroutine and returns the number of scheduled agents (the server logs
N agent(s) run on a schedule).
cron or topic, an expression that never matches, or one that runs more often than every 5 minutes (agent_defs.MinScheduleInterval) is an error when the definitions are loaded, and the server does not start.
The loop
tick(now) goes through the entries. For each one whose next is not after now it:
- takes
due = next, - sets
next = Schedule.Next(now): the next match after now, not afterdue, - calls
fire(entry, due).
next jumps to after now, an agent fires at most once per tick, even if several of its times have passed (the machine was suspended, the clock jumped, a previous fire was slow). The skipped times are not run later.
Cron times are whole minutes (Cron.Next truncates to the minute and goes to the one after), so the loop wakes at the due time itself, not at a polling interval; the 1-minute cap only bounds how long it sleeps blind.
Cron.Next looks for the next match with a bounded search (8 years; the zero time if there is none) and does not go back when a daylight saving gap makes time.Date answer an earlier time.
Starting a run: fire
For an entry that is due at due:
- Is the previous run still working? Each entry keeps
lastRun, the ID of the research its last successfulfirestarted (0 at startup). If it is set,fireloads that run (Manager.Get(user, lastRun)); if its status isrunning(Status.Active()), the run is skipped and a warning is logged (scheduled run skipped, the previous one is still working). If the run cannot be loaded, it is not treated as working. - The topic:
Schedule.TopicAt(due)renders thetopictemplate with{{.Date}}(YYYY-MM-DD) and{{.Time}}(RFC 3339), both of the due time in the schedule’s time zone. A template error skips the run (logged as an error). - The start:
Manager.Start(ctx, schedule.User, Options{Topic, Agent: <agent name>}), the same call asPOST /v1/gb/research. The research goes on in the background;firereturns as soon as it is recorded. On successlastRunis set to the new run’s ID andscheduled run startedis logged with therun_id.
scheduled run could not be started) does not stop the scheduler and is not retried: the entry is already moved on to its next time. Everything Start checks applies, so a scheduled run can also fail because, for example, the agent’s cost caps or the per-user limit of working researches (MaxActivePerUser, shared with the runs of the same user that people start) is reached, or because the server is shutting down.
Each entry has its own mutex around fire, so the check and the start of one agent do not interleave.
What it guarantees and what it does not
Only the server runs schedules.
gligabao_agent ignores the section, and an agent used only as a sub-agent role is never started by the scheduler. The schedule is not part of agent_hash.
Seeing it
- Logs (logrus, all start with
agent research:):scheduled agent(at startup, with the firstnext),scheduled run started(withrun_id),scheduled run skipped, the previous one is still working,scheduled run skipped, the topic cannot be made,scheduled run could not be started, and the warning when there is no LLM. GET /v1/gb/research/agents: thescheduleof each agent, withnext_run.- The runs: ordinary researches of the schedule’s
user, withagent_nameset (GET /v1/gb/research,gligabao research ls). There is no separate record of the schedule.
Tests
internal/agent_defs/cron_test.go (the parser, Next, never-matching expressions, parse errors, MinGap) and internal/agent_research/agent_defs_test.go (the scheduler through export_test.go: runs start when due and the topic template is filled, and a run is skipped while the previous one works).