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:
- 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 (agit stash createcheckpoint, anchored so garbage collection cannot drop it). - 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.
- It commits. When the agent exits, DeckSpace runs
git add -Aand a commit on that branch, with--allow-emptyso a run that changed nothing still lands as a commit. - 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.
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 picker | Stored as | You supply | First run | Repeats |
|---|---|---|---|---|
| Run once (now) | once | nothing | immediately — the next scheduler pass, so within 30 seconds | no |
| Run at… | at | a date and time | at that time, interpreted in your local timezone | no |
| Every N minutes | interval | N, minimum 1 | N minutes from when you click Schedule — not now | yes |
| Nightly | nightly | HH:MM | the next occurrence of that time in UTC | yes, 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 want | Can you say it | Closest available |
|---|---|---|
| Every weekday at 9am | not expressible | Nightly at 09:00 — which also fires on Saturday and Sunday |
| A particular day of the week | not expressible | — |
| A day of the month, or a date each quarter | not expressible | A one-off Run at… per occurrence, recreated by hand |
| Twice a day at set times | not expressible | Two separate nightly routines, one per time |
| A cron expression | not expressible | — |
| A repeating job at a local wall-clock time | not expressible | Convert 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 time | yes | Run 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.
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”:
| Situation | What happens |
|---|---|
| The app was closed when a routine came due | The 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 times | It runs once, not once per missed slot. Five missed nights produce one run. |
| The app was open but two jobs were already running | The third waits for a free slot and starts on a later pass. |
| A job was mid-run when you quit or the app crashed | At 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
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.
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.
| Platform | Confinement mechanism | The Sandbox checkbox |
|---|---|---|
| macOS | sandbox-exec (Seatbelt), built in | shown unticked by default |
| Linux | bwrap (Bubblewrap), only when it is installed and answers --version | shown only when bwrap is present |
| Windows | no mechanism exists | not 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 CLI | Its registered prompt flag | Driven correctly by -p |
|---|---|---|
| Claude Code (the default) | -p is its print mode | yes |
| Antigravity | -p | yes |
| Copilot CLI | -p | yes |
| Qwen Code | -p | yes |
| Aider | --message | no — it is sent -p anyway |
| OpenCode | --prompt | no — it is sent -p anyway |
| Droid | exec | no — it is sent -p anyway |
| Cursor Agent, Amp, Grok CLI | none registered | untested here — verify before scheduling |
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.
| Status | Meaning |
|---|---|
| pending | Scheduled. The row shows the next due time — unless it has none, in which case see above. |
| waiting | A workflow step blocked behind another step, not yet eligible. |
| running | The agent is executing now. |
| done | The agent exited zero. |
| error | Non-zero exit, a timeout, or an interrupted run. |
| cancelled | You 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
| Limit | Value |
|---|---|
| Scheduler wake interval | 30 seconds — the granularity of every schedule |
| Jobs running at once | 2, across the whole app |
| Due jobs examined per pass | 10, oldest due first |
| Wall-clock cap per run | 30 minutes, then the process is killed and the job is an error |
| Result stored | last 12,000 characters |
| Minimum interval | 60 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
- 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.
- Confirm it got a branch. No branch chip means it ran in your working directory.
- Convert your time to UTC for anything nightly, and put a reminder in for the next daylight-saving change.
- Check the row shows a next time after you create it. Pending with no next time never runs.
- Decide about the sandbox explicitly. It is off unless you ticked it, and the global setting is not involved.
- Remember the app has to be open. If the machine sleeps or DeckSpace quits, the schedule stops until you launch it again.