Scheduling

Updated September 29, 2026

Some work should just happen on its own: the overnight test triage, the morning inbox pass, the nightly backup script. A schedule runs it at a time you choose, once or on a cadence, so it happens without you at the keyboard.

A schedule can run either of two things:

  • an agent prompt: crystl launches a fresh agent shard and sends the prompt as its first message.
  • a command: crystl opens a shard and runs a shell command in it, with no agent at all.

Pick the command form when the task is already decided. If the job is bash scripts/backup.sh, an agent adds nothing: it is slower, it spends tokens, it can do something slightly different each night, and the run lands in your token ledger as agent spend for work no agent did.

When the work needs several stages that hand off to each other, with declared inputs and outputs between them, that is a sequence instead, and a sequence can carry a schedule of its own as its trigger. The two words overlap: crystl schedule is this feature, while schedule is also one of the four trigger types a sequence can use.

What a schedule carries

Every schedule has a gem (the project it runs in), a time, a cadence, and a time zone. The rest depends on what it runs.

For an agent prompt:

  • Prompt: the task, sent as the agent’s first message when it launches.
  • Agent: Claude, Codex, or another supported agent.
  • Model: pin a specific model version if the task needs it.
  • Auto-approve mode: off, in gem, or all for the scheduled run.
  • Phone reminder (optional): ask the agent to finish with a card that reaches your phone as a push.
  • Isolated worktree (optional): run on its own branch so ordinary in-repo edits stay in a separate working directory while you’re away. This is not an OS sandbox; see isolated sessions.

For a command:

  • Command: one shell command, run in a fresh shard.
  • Isolated worktree (optional): the same option, if the command should run on its own branch.

A command schedule takes no agent, no model, and no phone reminder. Those are refused rather than ignored, so a schedule never quietly means something other than what you wrote. A phone reminder is text an agent writes at the end of its turn, and there is no agent here to write it.

Cadence and time zone

Pick the cadence that fits:

  • once: fire a single run at a specific date and time.
  • daily: every day at the chosen time.
  • weekdays: Monday through Friday.
  • weekly, monthly, quarterly, yearly: longer cadences, anchored to the date you pick.

Times are interpreted in the time zone you set (an IANA zone like America/Los_Angeles), so “9:00 AM” means 9:00 where you are, not UTC. Daylight saving is handled for you.

Rotating through a list

A schedule can hold an ordered list of payloads instead of one. Each firing takes the next entry and wraps at the end, so five prompts on weekdays give you a different task each weekday: one coherent schedule rather than five scattered ones.

Rotation works for prompts and for commands. A list is all prompts or all commands, never a mix.

In the desktop form, turn on rotate through a list and the payload editor becomes a list you can add to, remove from, and reorder with the up and down buttons. The schedule’s detail view numbers every entry and marks the one that fires next. Moving a row changes which payload runs next, because the position is what advances, not the row itself.

Limits: at most 31 entries, a cap on the total size of the list, and every command entry has to fit the same single-line limit a lone command does (1023 bytes). crystl checks all of that when you save, so a list that could not fire is refused at the form rather than at 2am on the one entry that breaks.

If you shorten the list past the entry that was next, the marker moves back to the last entry. It never resets to the start, so you don’t replay entries you have already seen.

Rotation counts positions, not weekdays

Read this before you depend on the pairing. A rotation advances one position per firing. It is not bound to the calendar, so the pairing between entry and weekday can drift apart:

  • a missed run (crystl not running, or the gem closed) catches up once and advances one position, so everything after it shifts by a day.
  • pausing a schedule and resuming it after an occurrence has gone by skips to the next future occurrence without catching up.
  • editing the time, the cadence, the first-run time, or the time zone can recalculate the timing and shift the phase.

If the alignment matters, check the detail view: it always tells you which entry is next, and you can reorder the list to put the one you want back in front. Binding a payload to a specific weekday is not something crystl can do yet.

Creating a schedule

Open settings, schedules, and add one. Choose the gem, then pick the target: agent prompt, command with no agent, or sequence. Fill in the payload for whichever you picked, toggle rotate through a list if you want several, toggle an isolated worktree if you want one, then set the time, cadence, and time zone. The panel lists every schedule so you can edit or cancel any of them.

When a schedule fires

At the due time crystl creates a fresh shard in the gem. A prompt schedule takes the normal agent, model, and approval path and sends the prompt, so you get a proper agent session picking up the task. A command schedule runs the command in the shard’s shell and nothing else.

crystl must be running and the target gem open for a schedule to fire. After sleep or a restart, an overdue schedule runs once as catch-up rather than firing repeatedly for every missed slot.

What you learn about a command run

A command schedule reports how the run went, which a prompt schedule cannot do (there is no finish line for an agent turn). You’ll see one of four outcomes in crystl schedule list, on the schedule’s row, and on the phone:

  • succeeded: the shell reported exit 0.
  • failed: the shell reported a non-zero exit, and you get the exit code.
  • no start seen: the shell never reported the command starting within the grace period. That is not the same as “it didn’t run”. A command runs perfectly well in a shell that never loaded crystl’s shell integration, and that looks identical from the outside.
  • unknown: the command ran and crystl could not see how it went. Never read this as success.

A failed run raises a card naming the command that actually ran, which matters for a rotation: the card names Tuesday’s entry even if Wednesday’s is now next. crystl keeps the shard for a failed run, so the scrollback is there for you to read. A clean run’s shard closes once crystl can see that nothing is still running in it.

From the CLI

The same schedules are scriptable through the crystl CLI, so an agent or a shell script can set them up:

crystl schedule add --gem myapp --at "09:00" --repeat daily --prompt "triage the morning inbox"
crystl schedule add --gem myapp --at "02:00" --repeat daily --command "bash scripts/backup.sh"
crystl schedule add --gem myapp --at "2026-07-21 16:00" --agent codex --model gpt-5.1-codex-max --approval in-gem --isolated --prompt "prepare the release branch"
crystl schedule list
crystl schedule cancel 7A4F2C91

--prompt and --command are the two target choices, and you pass exactly one of them. To build a rotation, repeat the flag:

crystl schedule add --gem myapp --at "09:00" --repeat weekdays \
  --prompt "monday: sweep the inbox" \
  --prompt "tuesday: triage open bugs" \
  --prompt "wednesday: review dependency updates" \
  --prompt "thursday: check release health" \
  --prompt "friday: write the week's summary"

schedule without a subcommand defaults to schedule list.

Availability

Scheduling is free up to 3 schedules on every tier, agent or command, rotating or not. A rotating schedule counts as one however many payloads it holds. Create, list, and cancel from the desktop, CLI, or phone, and existing schedules keep running. Past three, crystl schedule add returns 403 limit_reached; a Guild membership lifts the cap.