routines: array on an agent’s frontmatter. For the concept, see Routines.
Where routines live
Every routine is declared on the agent it runs for. Inside the agent file’s frontmatter:routines: array is read by Rundock’s scheduler and ignored by Claude Code. An agent file with routines works in plain Claude Code; the routines simply do not run there.
Frontmatter fields
Each entry in theroutines: array is a YAML object. These five fields are the ones you write by hand:
A routine made in the form, or approved in Rundock, also carries these. Rundock writes them; do not hand-edit the plan keys.
The whole
routines block is Rundock-only. Other tools that read agent frontmatter ignore it.
Schedule format
Theschedule field accepts only two patterns. Both are exact regex matches.
Examples that work
Examples that look correct but never fire
Timezone
The schedule is interpreted in the local timezone of the machine running Rundock. Atimezone key is recorded on a routine made in the form, but it does not change when the routine fires. If you run Rundock on a VPS in a different timezone from where you live, set the schedule for the VPS’s local time, not yours.
Scheduler behaviour
The scheduler ticks every 60 seconds. On each tick:- Rundock re-discovers all agents (this picks up routine changes without a restart).
- For every routine on every agent, the scheduler computes the next run time.
- If the next run time has come due, and nothing else about the routine refuses it (not enabled, paused, an unreadable schedule, no prompt), Rundock fires it.
lastRun guard. Daily routines do not re-fire the same calendar day. Weekly routines do not re-fire on the same weekday they last ran on. The guard is held in memory, filled from .rundock/routine-state.json when Rundock starts or you switch workspace, and written back after every run, so a restart does not fire a routine twice.
When a routine fires, it runs on its agent’s runtime. For a Claude Code agent, Rundock spawns a headless Claude Code subprocess with the routine’s prompt as the input message. The subprocess runs with --dangerously-skip-permissions because there is no user available to approve tool calls in real time. A routine on a Codex agent runs as a turn on the Codex CLI.
Where a routine’s permission card appears. Claude Code’s permission hooks still run for a routine, so a routine’s agent can still raise a card. A routine has no conversation, so the card is matched to its run and shown in the approvals dock, over whatever screen is open, under the routine’s name and the agent’s, such as “Morning Briefing, run by Ada”. It never appears inside a conversation. A request Rundock cannot match to a run is shown in the same dock as unattributed. An unanswered card is denied at the usual timeout, and once a request has timed out, been answered in any window, or lost the process that asked, its card says so and can no longer be answered.
Routines fire only while Rundock is running
This is the central limit and it shapes everything else. Two practical paths around this:- Run Rundock on a small VPS so the scheduler ticks 24/7. Reach it from any device through a browser.
- Use Anthropic’s Claude Code Routines, Anthropic’s managed scheduling layer, for repo-bound work that can run on their cloud.
What Rundock records about a run
When a routine fires, the spawned Claude Code subprocess produces output on stdout (stream-json) and stderr. What Rundock records instead:- The routine’s
lastRuntimestamp. - The routine’s
status:running,completed,failed,cancelled, orinterrupted.completedandfailedfollow the subprocess exit code, except that a run whose agent declares a failure is recorded asfailed(see Declaring that a run failed).cancelledis written when the run was stopped from outside it (see Routines), which is recorded as its own outcome rather than as a failure.interruptedis written when a run left markedrunningis loaded back and nothing answers for it, meaning Rundock closed mid-run: this is distinguishable from a run that actually failed. - The routine’s
durationin seconds. - A separate record under
.rundock/runs/of the files the run changed: the path, the tool that touched it, whether it was created or edited, and when.
Declaring that a run failed
A run’s status follows how the process ended, so an agent that hits a problem, says so, and exits cleanly is recorded ascompleted. When your routine’s skill can fail in a way the process cannot see, such as finishing without doing its job, have the agent declare it. Make the last line of the agent’s final message:
failed, with the reason as its error, and the routine’s own status reads failed too. The Routines list shows it as Failed, and the run’s record shows Reported failed, headed “The agent reported that this run failed.” with “The reason it gave:” and your reason. A run whose process itself failed reads Stopped early instead, so the two are told apart. The reason is optional: RUN STATUS: failed on its own records the failure and says no reason was given.
The rules, exactly:
- Only the last non-empty line of the final message counts. A declaration followed by more text, or a run that ends on a tool call rather than a message, declares nothing.
RUN STATUSis matched without regard to case, and markdown emphasis or a code span around the line is ignored. Any other wording is not a declaration.RUN STATUS: succeededis accepted and changes nothing.- A run that declares nothing is recorded exactly as before, so no existing routine changes state.
- A run that was stopped from outside stays
cancelled, whatever it declared. - It is read from the run’s session transcript, so it applies to Claude runs. A Codex run leaves no transcript and cannot declare a failure this way yet.
The Routines view
Routines is its own entry in the rail. Its sidebar lists scopes: All routines, then each agent that owns routines, with a count beside each. The main pane is the Routines list for the chosen scope, ordered by next run. Each row shows:- The routine as a sentence, such as
Every day at 5:00am, run: Morning briefing, with the owning agent’s name beneath it. - Once it has run at least once, what happened last time, in one of five tones (Ran, Caught up, Missed, Failed, Stopped) and a link to the record.
- Its controls: Run now, the pause switch, Edit schedule and Delete.
A routine that is not enabled carries a Turn on control instead, and the offer states what pressing it does, including whether it runs shortly after being turned on because its slot has already gone today. The offer is withheld anywhere turning it on would not actually start it (also paused, or its schedule is unreadable), so the control never promises something that would not happen.
name and its schedule in words, such as “Every day at 5:00am”, or the raw schedule string if it is not one the form offers. Selecting it opens the Routines view for that agent.
Running a routine now
Run now runs the routine once, straight away, through the same path a scheduled run takes, and leaves the same kind of record. It is on every row, including a routine that is paused, not enabled, or waiting for approval, because a run you pressed is attended. It is refused only when nothing could run: the routine has no prompt, it names arunOn this release cannot run, or a run of it is already going. The refusal is shown on the row.
A manual run moves no schedule. The lastRun guard is not written, and the next run stays exactly where it was, so a test run in the afternoon does not satisfy that evening’s slot. The row’s Ran, Caught up and Missed verdicts come from scheduled runs only. The run record carries trigger: manual for a pressed run and trigger: scheduled for the scheduler’s.
Approval and pausing
A routine made in the form is approved from the moment it exists. Editing itsprompt, its skill or its runOn lapses that approval, because an agent can edit a routine and nobody would otherwise know. Editing when it runs, its timezone, pausing it, turning it on or off, or editing the skill’s own file leaves the approval standing.
A routine whose approval has lapsed is paused, and its row takes the attention colour, says that what it runs has changed since you last approved it, and offers Review and resume, which approves the plan as it stands on disk. A routine nobody has approved yet, such as one a package brought in, says it is waiting for your approval before its first run, with the same Review and resume.
A pause you set yourself is quieter: the row dims, reads Paused, and Resume brings it back. Pausing is the switch on the row, and it is the only control that sets or clears paused.
Upgrading an existing workspace to a version with routines
If a workspace already carried routines written by hand before Rundock could run them,enabled is what decides whether the upgrade starts them.
- A block with no
enabledkey reads as not enabled. It is listed, keeps every word of its file, and does not run until turned on. This is the common case for a routine that sat next to a cron job doing the same work by hand: the upgrade does not start a second copy of it without being asked. - A block that already says
enabled: true(oryes, oron) is live, and the upgrade leaves it alone. - A block that says
enabled: false(orno, oroff) stays off, and the upgrade leaves that alone too.
schedule. A cron expression already there survives an upgrade exactly as written, and still does not fire: cron was never one of the two accepted forms.
Complete worked example
Here is a complete agent file with a single routine. Every field is present.- The schedule uses the daily form, two-digit zero-padded.
enabled: trueis what makes it live; without it, the routine would sit in the list waiting to be turned on.- The prompt is short. It assumes the agent’s body knows what “the morning briefing” means.
- The description is for whoever reads the file. Rundock does not show it, and it does not affect the scheduler.
Common patterns
Five recipes that work in practice.Morning briefing on the orchestrator
End-of-day summary
Weekly research digest
Weekly digest on a different day to avoid collisions
Multiple routines on one agent, staggered
Common pitfalls
Cron expressions never fire. The scheduler does not understand cron. A routine withschedule: 0 5 * * * parses fine, registers fine, and never runs. The routine’s row names the unreadable schedule rather than staying silent about it. If a routine appears to do nothing, the schedule string is the first thing to check.
Hours without a leading zero never fire. The pattern is exact. every day at 9:00 does not match. Always zero-pad.
Abbreviated and multi-day weekdays never fire. Capitalisation is safe: the schedule string is lowercased before matching, so every Friday at 04:00 works. But the parser only matches one full lowercased weekday word. every Fri at 04:00, every Mon-Fri at 09:00, and every weekday at 18:00 do not match.
A routine with no enabled: true sits waiting to be turned on. This is deliberate, not a bug: an absent key means not enabled, so a routine written by hand needs the field spelled out to actually run. The routines list offers a Turn on control on the row.
Rundock is closed for a whole day. The scheduler is in-process, and catch-up is same-day only: a daily routine missed for an entire calendar day is not replayed the next day, and a weekly routine missed on its weekday waits a week. Opening Rundock later on the same day is fine, the routine fires then. For routines that must run at a particular hour, or on days you might not open Rundock at all, host it somewhere always-on.
Routines that need their output read. Rundock discards stdout. If the agent does not write its output somewhere durable through tools, the run produces nothing the user can find later. Always design routines so the agent writes a trace.
Two routines on the same agent fire in the same minute. Both spawn Claude Code subprocesses concurrently. They do not share context or file locks. Stagger the schedules unless the routines are genuinely independent and idempotent.
Routine name changed after first run. The routine’s lastRun guard is keyed on agentId:name. Renaming a routine creates a new key, which means the new name has no run history and may fire immediately on the next tick if its schedule has already passed. To rename a routine without an immediate re-fire, do it just after a known successful run rather than just before the next due time.
Where to next
Routines concept
What routines are, what they are good for, and the limits worth knowing.
Set up a routine
A walkthrough of adding a routine through the form and confirming it fires.
docs/ROUTINES.md in the Rundock GitHub repo. It tracks any new fields and scheduler behaviour as they ship.