OpenRoutine
Your AI agents' crontab, as markdown.
OpenRoutine is the open-source alternative to Claude Code Routines and ChatGPT's scheduled tasks — the same unattended-agent idea, with the vendor locks removed. One markdown file per task, crontab syntax in the frontmatter, any AI coding agent underneath. Tasks live in your repo, run on your machine, and answer to no account, plan, or cap.
Website: openroutine.dev — hand-written HTML under
site/, no build step, in English and 简体中文.
Cloudflare Workers Builds watches this repository, so a push to main that touches site/
deploys it; one that touches only the Rust project does not trigger a build.
site/DEPLOY.md has the settings.
A task is a file
---
name: todo-digest
description: Nightly TODO/FIXME triage
cron: "0 2 * * *"
agent: claude
---
Review all open TODO and FIXME comments in this repository.
For any that are trivially fixable, fix them and open a pull
request. Summarize everything else in reports/todo-digest.md.
Save that in your repo as todo-digest.md, register it once with openroutine add todo-digest.md, and it is the complete definition — diffed, code-reviewed, and greppable like everything else you commit. A single Rust daemon (openroutine serve, or openroutine install once for boot) runs your registered task files, schedules everything in-process, runs each task through the agent CLI you configure, and serves a REST fire endpoint plus a local web UI for history, logs, and pause/resume. Swap agent: claude for agent: codex — one line in a diff — and the same task runs on the other vendor's agent, which is precisely the move neither vendor's scheduler will ever offer.
Compared to the vendors
| Claude Code Routines | ChatGPT scheduled tasks | OpenRoutine | |
|---|---|---|---|
| Where it runs | Anthropic-managed cloud (or org-hosted environments) | OpenAI cloud; project tasks run locally, but only while the desktop app is running | Your machine, under a headless daemon supervised by launchd/systemd |
| Where tasks are defined | Web UI / /schedule, stored in your claude.ai account |
Chat or the Scheduled page, stored in your OpenAI account | Markdown files in your repo |
| Agents | Claude Code only | GPT models only | Any agent CLI: Claude Code, Codex, Gemini, your own |
| Triggers | Schedules, API endpoint, GitHub events | Schedules and change-monitoring, hourly at most | Cron and one-shot schedules; REST fire endpoint |
| Management UI | claude.ai web UI | Scheduled page in the app | Local web UI, no account |
| Limits | Subscription usage, daily run caps | 3–15 active tasks by plan; unattended tasks may auto-pause | Whatever your hardware tolerates |
| Availability | Research preview, paid plans | Paid plans | Open source |
The trade is honest in every direction: Routines gives you cloud execution, GitHub-event triggers, and managed sandboxing; ChatGPT gives you a polished cross-device inbox and change-monitoring; OpenRoutine gives you local files, local execution, no caps, and the freedom to swap the agent.
Install
macOS and Linux only (Windows is an explicit non-goal). Homebrew, PyPI, and npm all drop in the same prebuilt binary; the cargo routes build from source and need a Rust toolchain.
brew tap soulmachine/tap https://github.com/soulmachine/openroutine
brew trust --formula soulmachine/tap/openroutine
brew install openroutine # prebuilt binary, no Rust needed
pip install openroutine # or from PyPI — the binary, in a wheel
npm install -g openroutine # or from npm
cargo install openroutine # or build from crates.io
cargo install --path . # or build from a checkout
The brew tap line carries a URL because this repo is its own tap rather than
a separate homebrew-* one, and Homebrew 6 wants a third-party formula trusted
before it will load it. Once installed:
openroutine init # writes a config and prints a sample task
openroutine add my-task.md # register a task file you wrote
openroutine list # see what would run
openroutine serve # run the scheduler in the foreground
openroutine install # or register it as a boot service, no sudo
init writes ~/.config/openroutine/config.toml and prints a sample task file
to copy from; add registers each task file you write, after validating it.
Nothing else to configure. Deploy covers leaving it running on a
machine you don't sit at.
Using it
openroutine list # every task, its schedule, and its health
openroutine status # is the daemon up, and what does it hold
openroutine reload # re-read the config and every task file
openroutine run <task> --dry-run # exactly what a run would do, spawning nothing
openroutine run <task> # fire one now, through the daemon
openroutine logs <task> --follow # tail the latest run
openroutine pause --all # stop everything firing, keep the daemon up
openroutine dashboard # the local web UI, no account
Tasks are files, so everything else is ordinary editing. A task re-reads its
own file as it is about to run, so the prompt you just fixed is the one that
runs — but nothing is watched or polled, so the schedule itself never moves
between runs. openroutine reload is what makes an edit visible everywhere
else, and add and remove ask for one themselves.
The one thing to remember: a task that isn't going to run can't notice that
you changed it. A finished one-shot, a broken task, and a task you switched
off with disabled: true all need openroutine reload — flipping disabled
back to false in the file does nothing on its own. (Turning a task off that
way is still the right move: it's a change your reviewer can see.)
The daemon also serves a REST API on 127.0.0.1:7373, guarded by a bearer
token (openroutine token), so alerting systems and git hooks can fire a task:
curl -X POST http://127.0.0.1:7373/v1/tasks/todo-digest/fire \
-H "Authorization: Bearer $(openroutine token)" \
-d '{"text": "Sentry alert SEN-4521 fired in prod."}'
The optional text reaches the agent labelled as caller-supplied context, not
as instructions — anyone who can reach the endpoint can send text, so text must
not be able to redefine the task.
Deploy
Everything above drives the daemon by hand. To leave it running unattended on a machine you don't sit at — the Mac mini under the desk, a home server — register it with the system's service manager:
cargo install --path . # install to a stable path; see below
openroutine init # write the config; prints a sample task
openroutine add hello.md # register a task to prove it works
openroutine install # register with launchd/systemd, no sudo
openroutine status # daemon: running (pid …)
install records the path of the binary that registers it, so run it from the
installed copy rather than from target/release/openroutine — a cargo clean
should not be able to unmake your scheduler. It writes a per-user service and
never asks for sudo. openroutine install --print shows exactly what it would
write, and what it would run, without writing anything.
Your agent must be on the login shell's PATH
The daemon runs every agent through a login shell, so a run gets the same
PATH, shims, and API keys your terminal has. A login shell is not an
interactive one: zsh reads .zshenv and .zprofile but not .zshrc, and
bash reads .bash_profile but not .bashrc. So an agent that only your
.zshrc puts on the PATH — anything in ~/.local/bin is the usual case —
is found when you test by hand and missing once launchd starts the daemon:
zsh:1: command not found: claude
openroutine list warns before you get there. It samples the login shell the
way a service manager starts one — with PATH seeded to the bare
/usr/bin:/bin:/usr/sbin:/sbin a daemon inherits, never the PATH your
terminal happens to have — so it answers for the daemon rather than for you:
warning: "claude" is on your PATH here but not under a service manager, so
scheduled Runs will fail; move its PATH export into your login profile
Fix it by moving the PATH export into .zprofile, which repairs SSH and
cron sessions at the same time, or by naming the agent absolutely:
[agents.claude]
cmd = "/Users/you/.local/bin/claude -p {prompt}"
Verify the way the daemon will see it — a login shell with none of your terminal's inherited environment:
env -i HOME="$HOME" SHELL=/bin/zsh PATH=/usr/bin:/bin /bin/zsh -lc 'command -v claude'
macOS
install writes a LaunchAgent to ~/Library/LaunchAgents/, so the daemon
starts at login rather than at boot, and launchd restarts it if it dies. On
a headless machine, pair it with auto-login (System Settings → Users & Groups
→ Automatically log in as), which requires FileVault to be off.
Auto-login is not only about the daemon starting. Agent CLIs keep credentials
in your login keychain, and that keychain is unlocked by the GUI login — a
service that starts without one finds it locked, and the agent reports itself
logged out. That is also why install does not write a root LaunchDaemon:
starting before anyone logs in is precisely the state in which the agent
cannot authenticate, so the one thing a LaunchDaemon buys is the one thing
that breaks it. The trade runs the other way too, and it is a real one:
auto-login with FileVault off means physical access is a logged-in desktop.
While you are there, stop the machine sleeping through its own schedules:
sudo pmset -c sleep 0 displaysleep 0 disksleep 0 # never sleep
sudo pmset -c autorestart 1 womp 1 # return after power loss
launchctl print gui/$(id -u)/dev.openroutine.daemon | grep state
Linux
install writes a systemd user unit and enables lingering, so the daemon
starts at boot with no login session — the one platform where "no login
needed" holds without an asterisk.
systemctl --user status dev.openroutine.daemon
journalctl --user -u dev.openroutine.daemon -f
Confirming it survives
openroutine run <task> # a real run, end to end
openroutine logs <task> # what the agent actually printed
Killing the daemon outright is a fair test: the service manager should bring
it back within seconds under a new pid. openroutine uninstall unregisters
it and leaves your config, state, and tasks untouched.
Status
Implemented: scheduler, runner, REST API, and web UI, in one binary. .scratch/openroutine-v1/spec.md is the full design; CONTEXT.md is the glossary; docs/adr/ records the architectural decisions.
1.0 breaks with 0.2. Tasks are registered one file at a time rather than
by directory, so a [[projects]] config no longer loads — register each file
with openroutine add <file.md>. Task files now need a name:, their id is
derived from it, and timeout: is gone: a run is bounded by silence
(idle_timeout in config, 15m) rather than by total runtime. Run history from
before the change is orphaned rather than migrated.
1.0 means the design has settled, not that the surface is frozen. The
frontmatter schema, the CLI, the REST API, and the on-disk layout are all
documented and meant to last — but while the project has essentially no
installed base, a bad name is worth fixing rather than carrying to 2.0.
Breaking changes get a release note and a line in DECISIONS.md; they do not
get a version-number promise they would only strain against.
Known limits, all deliberate: no GitHub-event triggers and no notifications (the fire endpoint is the integration point); state is written atomically against a killed process but is not fsynced against power loss; the web UI's rendering is verified by hand rather than by a browser harness.
License
See LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openroutine-1.0.5-py3-none-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: openroutine-1.0.5-py3-none-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 2.8 MB
- Tags: Python 3, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fc3a0ceb4e2fb24bdd448f939825a02674c82bf23fef71fb953a2636578d23f7
|
|
| MD5 |
88b130d4b23355a95f18b3c8b0cf1fba
|
|
| BLAKE2b-256 |
cd42f683e0cc5529696b92d4364f0c35fb634db31ef076f9df253fcf1159a92e
|
Provenance
The following attestation bundles were made for openroutine-1.0.5-py3-none-manylinux_2_34_x86_64.whl:
Publisher:
release.yml on soulmachine/openroutine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openroutine-1.0.5-py3-none-manylinux_2_34_x86_64.whl -
Subject digest:
fc3a0ceb4e2fb24bdd448f939825a02674c82bf23fef71fb953a2636578d23f7 - Sigstore transparency entry: 2495412373
- Sigstore integration time:
-
Permalink:
soulmachine/openroutine@310cf6e135f532095391829ba0c6a1801d2c1478 -
Branch / Tag:
refs/tags/v1.0.5 - Owner: https://github.com/soulmachine
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@310cf6e135f532095391829ba0c6a1801d2c1478 -
Trigger Event:
push
-
Statement type:
File details
Details for the file openroutine-1.0.5-py3-none-manylinux_2_34_aarch64.whl.
File metadata
- Download URL: openroutine-1.0.5-py3-none-manylinux_2_34_aarch64.whl
- Upload date:
- Size: 2.8 MB
- Tags: Python 3, manylinux: glibc 2.34+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22a8ae1be9d271602f996510679b608f42b105980de2d751a300401dcd52b024
|
|
| MD5 |
7a433e1e65d7c8ca1df105d41684d666
|
|
| BLAKE2b-256 |
a40e28b4f5e57427e60e6f97202c4c3c8a498bffb46257dda7e6debb68718e9d
|
Provenance
The following attestation bundles were made for openroutine-1.0.5-py3-none-manylinux_2_34_aarch64.whl:
Publisher:
release.yml on soulmachine/openroutine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openroutine-1.0.5-py3-none-manylinux_2_34_aarch64.whl -
Subject digest:
22a8ae1be9d271602f996510679b608f42b105980de2d751a300401dcd52b024 - Sigstore transparency entry: 2495412356
- Sigstore integration time:
-
Permalink:
soulmachine/openroutine@310cf6e135f532095391829ba0c6a1801d2c1478 -
Branch / Tag:
refs/tags/v1.0.5 - Owner: https://github.com/soulmachine
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@310cf6e135f532095391829ba0c6a1801d2c1478 -
Trigger Event:
push
-
Statement type:
File details
Details for the file openroutine-1.0.5-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: openroutine-1.0.5-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.5 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eb6773eeac11d245d9f402fee3c9d6868f68f67431b83cb4ca07e511fa6e3493
|
|
| MD5 |
cdeb79c7930db8d24715931b404421b7
|
|
| BLAKE2b-256 |
cb44dda268a1aab5768c2dccc380921fc7c241d950fe611f69a0f4d35fd2de5e
|
Provenance
The following attestation bundles were made for openroutine-1.0.5-py3-none-macosx_11_0_arm64.whl:
Publisher:
release.yml on soulmachine/openroutine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openroutine-1.0.5-py3-none-macosx_11_0_arm64.whl -
Subject digest:
eb6773eeac11d245d9f402fee3c9d6868f68f67431b83cb4ca07e511fa6e3493 - Sigstore transparency entry: 2495412352
- Sigstore integration time:
-
Permalink:
soulmachine/openroutine@310cf6e135f532095391829ba0c6a1801d2c1478 -
Branch / Tag:
refs/tags/v1.0.5 - Owner: https://github.com/soulmachine
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@310cf6e135f532095391829ba0c6a1801d2c1478 -
Trigger Event:
push
-
Statement type:
File details
Details for the file openroutine-1.0.5-py3-none-macosx_10_12_x86_64.whl.
File metadata
- Download URL: openroutine-1.0.5-py3-none-macosx_10_12_x86_64.whl
- Upload date:
- Size: 2.6 MB
- Tags: Python 3, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b6016677e71889023e42a6ca3423b5cf4b55ea4c24402ef2a073bfcfa12025cd
|
|
| MD5 |
f9694d71f3db719ba5d0420562947943
|
|
| BLAKE2b-256 |
bef283d525bb5c0115038b0af0998fe04ba80d3ca730b97a988e353aee0bd1a0
|
Provenance
The following attestation bundles were made for openroutine-1.0.5-py3-none-macosx_10_12_x86_64.whl:
Publisher:
release.yml on soulmachine/openroutine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openroutine-1.0.5-py3-none-macosx_10_12_x86_64.whl -
Subject digest:
b6016677e71889023e42a6ca3423b5cf4b55ea4c24402ef2a073bfcfa12025cd - Sigstore transparency entry: 2495412368
- Sigstore integration time:
-
Permalink:
soulmachine/openroutine@310cf6e135f532095391829ba0c6a1801d2c1478 -
Branch / Tag:
refs/tags/v1.0.5 - Owner: https://github.com/soulmachine
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@310cf6e135f532095391829ba0c6a1801d2c1478 -
Trigger Event:
push
-
Statement type: