Routines

A routine is one prompt, one agent, on a schedule. There are four schedule shapes, they are UTC, and they have no concept of a day of the week.

In the app this feature is called Background Agents. Open it from the command palette (Background Agents) or the right dock (Background — the clock icon). The panel is titled Background Jobs. Nothing in the product is labelled “routines”; this page uses the word for the thing you schedule.

It needs an active licence. Without one the Schedule button is disabled and the panel says so.

What a routine actually runs

A routine is not a shell command and not a pane. When it comes due, DeckSpace runs your agent CLI once, headlessly, and captures its output:

  1. It makes a worktree. If the project is a git repo, DeckSpace creates a branch named bgjob/<job-name>-<hex> and a fresh git worktree for it under your temp directory, then takes a snapshot of the base repo (a git stash create checkpoint, anchored so garbage collection cannot drop it).
  2. It runs the agent in that worktree. The command is built by the same headless builder the rest of the app uses, and it is the same isolation swarm uses — a separate checkout of your repo, so the run edits its own copy.
  3. It commits. When the agent exits, DeckSpace runs git add -A and a commit on that branch, with --allow-empty so a run that changed nothing still lands as a commit.
  4. It records the result. Stdout and stderr are stored on the job and shown behind the Result toggle. The exit code decides done versus error.
The worktree is best-effort, and its absence is silent

If the project is not a git repo, or if creating the worktree fails, the run falls back to your actual working directory. There is no message, no warning and no failed job — the agent runs in your live tree instead of a copy.

The only signal is the branch chip in the job row. A finished job with no branch name next to the agent name ran in your working directory. Check for it before you schedule anything that writes.

The four schedules

The picker offers exactly these. There is no other way to schedule a job in the UI.

In the pickerStored asYou supplyFirst runRepeats
Run once (now)oncenothingimmediately — the next scheduler pass, so within 30 secondsno
Run at…ata date and timeat that time, interpreted in your local timezoneno
Every N minutesintervalN, minimum 1N minutes from when you click Schedule — not nowyes
NightlynightlyHH:MMthe next occurrence of that time in UTCyes, every 24 hours

Intervals shorter than 60 seconds are floored to 60 seconds. An interval reschedules from the moment the previous run finished, so a 60-minute routine whose run takes four minutes drifts four minutes later each cycle. Nightly recomputes from the clock instead, so it does not drift.

“Every weekday at 9am” cannot be expressed

This is the first thing most people try, and it is worth being blunt: there is no way to write it. A schedule is one of four kinds, and none of them carries a day of the week.

What you wantCan you say itClosest available
Every weekday at 9amnot expressibleNightly at 09:00 — which also fires on Saturday and Sunday
A particular day of the weeknot expressible
A day of the month, or a date each quarternot expressibleA one-off Run at… per occurrence, recreated by hand
Twice a day at set timesnot expressibleTwo separate nightly routines, one per time
A cron expressionnot expressible
A repeating job at a local wall-clock timenot expressibleConvert your 9am to UTC by hand, and change it again when your offset shifts for daylight saving
A one-off at a local wall-clock timeyesRun at… already uses your local clock

So the honest answer to “every weekday at 9am” is: create a nightly routine at your 9am converted to UTC, accept that it also runs at the weekend, and write the prompt so a weekend run is harmless — or have the agent check the day itself and stop.

The two time fields disagree with each other

Nightly is UTC. The panel says so in small grey text next to the field, and the backend compares your HH:MM against seconds-since-UTC-midnight.

Run at… is local. It uses a browser date-time picker whose value is converted with the local timezone, then stored as an absolute timestamp.

Both are correct on their own terms and neither is labelled clearly enough. If you are not on UTC, assume every repeating routine you create is offset from the time you had in mind.

Routines run only while DeckSpace is open

The scheduler is a thread inside the app, and every job is a child process of the app. Quit DeckSpace and nothing is scheduled any more. There is no launch agent, no daemon and no cron entry — closing the laptop lid or quitting the app stops the whole system.

What happens to a run you were closed for is more specific than “it is skipped”:

SituationWhat happens
The app was closed when a routine came dueThe due time stays on the job. The first scheduler pass after you reopen the app sees it as overdue and runs it then, at whatever time that is.
The app was closed across several due timesIt runs once, not once per missed slot. Five missed nights produce one run.
The app was open but two jobs were already runningThe third waits for a free slot and starts on a later pass.
A job was mid-run when you quit or the app crashedAt the next launch it is settled, not resumed. A one-off becomes error with the note interrupted — DeckSpace restarted while this job was running. A recurring job returns to pending at its next scheduled slot, so the schedule survives.

A one-off is deliberately not retried after an interruption, because the run had already created a worktree, snapshotted and possibly committed. If you want it retried, press Run now.

The routine that never runs

Read this one twice

Pick Run at… and leave the date field empty, and the Schedule button still works. The job is created, appears in the list, and shows the status pending — and it will never run, on any day, for any reason. The empty date produces an unparseable timestamp, the job is stored with no due time, and the scheduler only ever selects jobs that have one.

Nothing tells you. There is no error, no red dot, and the word pending is exactly what a correctly scheduled job shows too. The tell is the missing next time: a healthy pending job reads next in 4h; this one reads only pending.

If you see a pending job with no next time, it is dead. Delete it and recreate it, or press Run now, which gives it a due time and unsticks it permanently.

The sandbox flag is per job, and it is off

Each routine carries its own Sandbox checkbox, next to Desktop notification when done. It is unticked by default, in the form and in the backend.

Settings → Autonomy does not reach routines

The app-wide sandbox switch is read by agent panes, by Maestro and by missions. Background jobs do not read it at all. Turning the global sandbox on does not fence a single routine, and turning it off does not unfence one.

The per-job checkbox is the only control, it defaults to off, and it applies to that job alone. A routine you scheduled without ticking it runs your agent with no filesystem confinement, whatever the global setting says.

When you do tick it, two independent things happen:

  • Filesystem write confinement, if the operating system provides a mechanism. Reads, execution and network stay open; writes are denied everywhere except the run directory, the agent’s own config and cache directories, and temp.
  • Egress filtering, which points the agent’s proxy environment variables at a small local filter that refuses link-local and cloud-metadata addresses. This one is applied whenever you tick the box, even where no filesystem sandbox exists.

The egress filter is fail-open by construction: it only affects agent CLIs that honour HTTPS_PROXY. A CLI that ignores proxy variables connects directly and is not filtered. Treat it as a layer, not a boundary.

PlatformConfinement mechanismThe Sandbox checkbox
macOSsandbox-exec (Seatbelt), built inshown unticked by default
Linuxbwrap (Bubblewrap), only when it is installed and answers --versionshown only when bwrap is present
Windowsno mechanism existsnot shown at all — the run is never confined

The panel hides the checkbox entirely when no mechanism is available, so on a machine without one there is nothing on screen saying the routine is unconfined. What protects your repository in that case is the worktree, not a fence — and only when the worktree was actually created. See What walk-away does for the same distinction applied to panes.

Which agent CLI you pick matters

The dropdown lists every agent CLI DeckSpace found on your machine. The headless run always invokes it the same way — the binary, then -p, then your prompt — regardless of which flag that CLI actually uses for a non-interactive prompt. The per-agent prompt flag DeckSpace keeps for pane launches is not consulted here.

Agent CLIIts registered prompt flagDriven correctly by -p
Claude Code (the default)-p is its print modeyes
Antigravity-pyes
Copilot CLI-pyes
Qwen Code-pyes
Aider--messageno — it is sent -p anyway
OpenCode--promptno — it is sent -p anyway
Droidexecno — it is sent -p anyway
Cursor Agent, Amp, Grok CLInone registereduntested here — verify before scheduling
Test any new agent with a one-off first

Create the job as Run once (now) and read the Result before you turn it into a nightly. A CLI that rejects -p shows up as an immediate error with its own usage text in the result. A CLI that instead sits waiting for input burns the full 30-minute timeout and then reports a timeout — every night, until you notice.

Reading the result

The job row carries the schedule kind, the agent binary, the branch, and a status dot. Expand Result for the captured output.

StatusMeaning
pendingScheduled. The row shows the next due time — unless it has none, in which case see above.
waitingA workflow step blocked behind another step, not yet eligible.
runningThe agent is executing now.
doneThe agent exited zero.
errorNon-zero exit, a timeout, or an interrupted run.
cancelledYou cancelled it before it started.

Three things about the result are worth knowing before you rely on it:

  • The diff is a git branch, not a view in the app. The panel shows you the branch name and the agent’s stdout. To see what changed, use git, or your normal review tools, against that branch. There is no in-app diff for an ordinary routine.
  • Long output is truncated from the front. Only the last 12,000 characters are kept, prefixed with …[truncated]. An agent that prints a lot loses its own opening.
  • The desktop notification is suppressed while the window is focused. That is deliberate — notifications only fire when you have tabbed away — but it means ticking the box and then watching the app produces no notification at all.

Branches and worktrees are not cleaned up. Every run of a nightly routine leaves another bgjob/… branch behind, and deleting the routine does not delete them. Prune them yourself. The worktree directories live under your temp directory and macOS may reap them, but the commits are in your repository, so the branches survive their directories.

Hard limits

LimitValue
Scheduler wake interval30 seconds — the granularity of every schedule
Jobs running at once2, across the whole app
Due jobs examined per pass10, oldest due first
Wall-clock cap per run30 minutes, then the process is killed and the job is an error
Result storedlast 12,000 characters
Minimum interval60 seconds

A running job cannot be stopped from the panel. Cancel is offered only for jobs that have not started; once a job is running, the 30-minute cap and quitting the app are the only ways it ends. Delete removes the job row at any time but does not stop a run in flight.

Workflows are routines too

A workflow is a YAML file under .deckspace/workflows/ whose steps are dispatched as background jobs on the same scheduler, chained by depends_on. Two things follow from that and are worth stating here:

  • A workflow cannot be scheduled. Its schema has a name, a description and steps. There is no schedule field, and every step-job is created as a one-off, due now. Workflows run when you run them.
  • Workflow steps are never sandboxed. Each step-job is created with the sandbox flag off, and the YAML schema has no field to turn it on. The per-job checkbox in the Background Agents panel does not apply to them.

Before you schedule something unattended

  1. Run it once first. Run once (now), read the result, then convert it to a schedule. Most routine failures are the agent CLI, not the scheduler.
  2. Confirm it got a branch. No branch chip means it ran in your working directory.
  3. Convert your time to UTC for anything nightly, and put a reminder in for the next daylight-saving change.
  4. Check the row shows a next time after you create it. Pending with no next time never runs.
  5. Decide about the sandbox explicitly. It is off unless you ticked it, and the global setting is not involved.
  6. Remember the app has to be open. If the machine sleeps or DeckSpace quits, the schedule stops until you launch it again.