Skip to content
RoutineOS
Esc
↑↓navigate↵open⌘Jpreview
On this page

Scheduling and Claude cloud routines

How a routine runs on Claude scheduled tasks. Triggers, cron in UTC, the hourly limits, environments, network access, secrets, setup scripts, creation with RemoteTrigger and the first report-only run.

What it is

Every RoutineOS routine runs as a Claude scheduled task. Anthropic’s docs now call these “scheduled tasks, formerly routines”, and the old name still appears in places such as the URL claude.ai/code/routines and the /routines alias for /schedule (docs, fetched 2026-10-30). A scheduled task is a saved Claude Code configuration (a prompt, one or more repositories and a set of connectors) that runs on Anthropic-managed cloud infrastructure, so it keeps working when your laptop is closed.

Scheduled tasks are a research preview: “Behavior, limits, and the API surface may change.” They are available on Pro, Max, Team and Enterprise plans (same source).

RoutineOS keeps the word “routine” for its own unit. A RoutineOS routine is the folder, the Desk and the rules; the scheduled task is where it runs.

Why it exists

A watch that only runs when someone remembers to run it is not a watch. Scheduled tasks give a routine a clock, a clean machine each time, and connectors, without anyone hosting a server.

How it works

Triggers

Per the docs (fetched 2026-10-30), a task can have one or more triggers:

Trigger What it does How RoutineOS uses it
Schedule A recurring cadence or one future time. Custom cron is allowed; expressions that run more often than once per hour are rejected The routine’s schedule
API An HTTP POST to the task’s own endpoint with a bearer token, under the beta header experimental-cc-routine-2026-04-01 Optional; added in the web UI by the person
GitHub Repository events such as pull_request.opened or releases Not used by the shipped templates

The registry records the API trigger as api-trigger-fire (docs-stated): text passed in a fire is delivered as untrusted data, and the saved prompt must opt in to act on it (fire-payload-untrusted). A fire is never approval or consent.

Cron is UTC

When a cron expression is five fields alone, its hours are UTC, so 0 9 * * 1-5 runs at 9:00 UTC on weekdays (docs, fetched 2026-10-30). A run scheduled exactly on the hour can start several minutes late; the docs suggest a few minutes past the hour.

RoutineOS stores both: schedule (in routine.toml, repeated on the [[routine]] entry in app.toml) is the UTC cron, and timezone on the [[routine]] entry is the person’s IANA zone, for example Australia/Melbourne. The permission screen shows the schedule in local time and in UTC with runs per day. lint refuses a schedule whose runs are not at least 1 hour apart (fixture tests/fixtures/fast_schedule, */30 * * * *).

Limits

Separately from subscription usage, each way of starting a run has an hourly limit (docs, fetched 2026-10-30):

Start Limit Counted per
Scheduled runs, including one-off runs 100 per hour Account
Run now, API fires and re-running a one-off task 30 per hour Each task, one count shared by all three
Run now and re-running a one-off task 100 per hour Account
API fires 100 per hour Account, counted separately from Run now

None of these has overage. GitHub events have their own per-task and per-account hourly caps, and events over the cap are dropped. RoutineOS adds its own daily ceiling, [budget] max_runs_per_day, checked in the run by budget.py decide (prompt-enforced, not guaranteed) (see Code routines). The registry records that no automatic pause on cost overrun is known (auto-pause-on-cost, unverified) and that the maximum run time is not known (max-run-time, unverified).

What a run is

Runs are full Claude Code cloud sessions: shell commands, skills and any included connectors. Skills committed to the repository’s .claude/skills/ load; skills only in ~/.claude/skills/ on a machine do not (repo-skills-carry-over, docs-stated). Plugins a repository turns on are not installed in a cloud session (repo-plugins-not-installed-in-cloud, docs-stated).

A run starts in auto mode unless it is unavailable. It still stops and waits when an action matches an ask rule, or when blocks reach auto mode’s repeated-block thresholds (docs, fetched 2026-10-30). An unattended run that waits is a stalled run, so RoutineOS designs routines to avoid prompts: report-only first runs, gates that resume in a later run, and a tools list on each routine.

Tasks belong to one claude.ai account and are not shared with teammates. Anything a task does through GitHub or connectors “appears as you” (same source).

Environments and secrets

An environment controls a run’s network access, its environment variables and its setup script. The docs describe a Default environment with Trusted network access, which allows a default allowlist of package registries, cloud provider APIs and common development domains. In a pilot, the Default environment’s dialog showed Limited with no domains. The registry records this as a conflict (default-environment-network), so RoutineOS never assumes the level.

RoutineOS cannot read an environment’s settings. RemoteTrigger returns only an environment id and an empty variables object (environment-settings-not-readable-via-api, pilot-verified). So before the permission screen, the person opens the environment dialog on claude.ai and reads out its four controls:

  1. Network access: the level and allowed domains.
  2. Network secrets.
  3. Setup script.
  4. Environment variables.

RoutineOS records the answers in [environment] in app.toml, with variable names only, never values:

[environment]
name = "hiring-signals"
network = "limited"          # none, trusted, limited, full or custom
allowed_domains = []
setup_script = false
env_vars = []                # names only

lint refuses a value in env_vars: “names only, never values; remove the value”. A Default environment (or shared = true) gets the shared-environment warning: the Default cannot be renamed or archived and is shared by every routine on it, so a change to it affects all of them (default-environment-shared, pilot-verified).

The secrets ladder, best first:

Rung Where the key lives
1 A connector. No key reaches the run
2 A network secret in a per-app environment named for the app
3 A network secret in a shared environment, with a loud warning
never A plain environment variable, a file, or the prompt

Network secrets attach the key through Anthropic’s proxy outside the VM, never as environment variables or files (network-secrets, docs-stated). Plain environment variables are readable by anyone who uses the environment (env-vars-readable, docs-stated). Environments are created in the web UI only (environments-ui-only). RoutineOS never asks for a key in chat; if one is pasted, it warns at once to rotate it and does not use or echo it.

Each secret is declared in app.toml with the single host it may reach:

[[secret]]
name = "jobs-board-key"
where = "network-secret"   # connector or network-secret, never env-var
host = "api.example.com"

Setup scripts

A setup script is a Bash script that runs when a new cloud session starts, before Claude Code launches. Per the registry record setup-script-rules (docs-stated): up to 64 KB, it must exit zero, it is cached only if it ends in about 5 minutes, the environment’s variables are not set yet, and the cache keeps files only. Runs without one log “No setup script configured” (pilot).

Network reachability

A run’s network behaves like an allowlist. In a pilot, api.cloudflare.com, gateway.ai.cloudflare.com, developers.cloudflare.com and huggingface.co were blocked with a proxy 403 while some package hosts answered (sandbox-allowlist, pilot-verified). Connector traffic goes through Anthropic’s servers, not the environment’s network, so a connector does not depend on the level (connector-traffic-bypasses-sandbox-network, docs-stated).

Probe before you design. Reachability from a run is the only proof. Before a design depends on a host, run one report-only probe from a run in the same environment: fetch one URL, record the status and any x-deny-reason, and write the result to risks.md.

Creating the task

RoutineOS creates tasks with /schedule and the RemoteTrigger tool (actions list, get, create, update, run, list_runs, get_run_log), in the person’s interactive session with a claude.ai login (remotetrigger-manage-tasks, docs-stated). The tool is absent inside a run, so a run can never fire or edit routines (run-cannot-fire-routines, pilot-verified).

  1. list first. If a task with the app’s name exists, ask whether to update it or create another. Never create a duplicate silently.
  2. Create the schedule trigger with the approved cron, repo, prompt, environment and connector subset.
  3. API and GitHub triggers are added by the person in the web UI. Record that the trigger exists, never the token.
  4. Read back with list and confirm the id, schedule, repo, environment and connectors match the permission screen. Store the id in [deployed].

Pilot findings on the request body (remotetrigger-body-shape, pilot-verified): give repos as session_context.sources: [{"git_repository": {"url": ...}}]. The shape {"type": "git_repository", "url": ...} at create is dropped without an error. An update replaces job_config.ccr whole, so resend events and sources with every update or the prompt is wiped. Read back after every create and every update.

The first run

Every run before state/verified exists is report-only. RoutineOS runs the task once immediately, reads the run log and the Desk, and reports exactly what happened: items found, written, skipped and why. The person decides whether it is what they wanted, and only their word writes state/verified. Moving to sending, posting or deleting is a separate step with its own permission screen.

Worked example

Hiring Signals runs Monday and Thursday:

[routine]
type = "watch"
schedule = "0 22 * * 1,4"
first_run = "report-only"

The cron is UTC: 22:00 UTC on Monday and Thursday, which is Tuesday and Friday morning in eastern Australia (the exact local hour depends on daylight saving). That is about eight runs a month, far under every hourly limit. With [budget] max_runs_per_day = 1 in its app.toml, the prompt header has the run call budget.py decide, which returns "allow": false for a second run on the same day whatever the session says. A pilot saw a run exceed its cap when a message in the session told it to (run-obeys-in-session-cap-override), which is why the count lives in a script. The run still has to call the script, so the cap is prompt-enforced, not guaranteed.

The person uses a new environment named hiring-signals with no network secrets, because the routine needs only web search and connectors.

Reference

Registry id Status Claim, short
schedule-limits docs-stated 1 hour minimum, 100 scheduled per hour per account, 30 per hour per task
run-vm-spec docs-stated Fresh VM per run, repo cloned each run
environment-settings-not-readable-via-api pilot-verified RoutineOS cannot read the environment
default-environment-network conflict Default network level
default-environment-shared pilot-verified Default is shared and cannot be renamed
setup-script-rules docs-stated 64 KB, exit zero, about 5 minutes
remotetrigger-body-shape pilot-verified Repo shape and whole-replace updates
max-run-time unverified Not known
auto-pause-on-cost unverified Not known

Common pitfalls

  • Writing local hours in the cron. Five-field cron is UTC.
  • Scheduling exactly on the hour. It can start several minutes late.
  • Changing the Default environment. Every routine on it changes too. Say so before changing it.
  • An update that drops sources. The prompt is wiped. Always read back.
  • A key in an environment variable. Anyone who uses the environment can read it. Use a network secret.

Was this page helpful?