orchestratr
concepts

loops (scheduling model)

durable cron for any command. how a loop owns time, how runs get ids and process groups, and how overlap, timezones, and missed fires are handled.

a loop is durable cron for any command. it survives the shell that created it and fires on a cadence you set. the loop owns time only: no provider flags, no prompts, no judge logic, no stop-condition DSL. the command it fires spawns agents via the CLI or SDK like any other caller.

this page is the model. for the full verb set (create, pause, resume, rm, ls, logs, and the loop run sub-noun) see schedule work with loops.

a loop owns time; the command does the work

orcr loop create nightly "0 9 * * *" --overlap queue -- \
  node_modules/.bin/tsx ~/.orcr/workflows/nightly/triage.ts

the -- boundary captures an argv array, executed directly with no shell. want shell features? say so: -- sh -c 'a && b'. when the cadence fires, orcr runs that argv; the script inside spawns agents, waits on them, reads their output. the loop never sees an agent.

runs and run ids

every run (scheduled or manual) gets a run id: r plus 5 lowercase alphanumeric characters (for example r82c9s; the r prefix makes run ids recognizable in a path), unique within the loop, plus a uuid in the store. the run's path is <loop_name>/<run_id>, for example nightly/r82c9s. that path is its handle everywhere: log tags, --run filters, and the scope for its agents.

the run command executes in its own process group (pid and pgid recorded) with ORCR_PATH=<loop_name>/<run_id>, so every agent it spawns lands under that path. a script's --path review/file_1 yields nightly/r82c9s/review/file_1. orcr agent ls --all nightly is the loop's full agent history. the run's cwd is the loop's recorded creation cwd (deliberately the workspace, not the script's folder), which agents inherit as their default cwd; a script living elsewhere is invoked by absolute path. see data and file conventions.

loops are always root-level

a loop's name is its level-1 path, and it is mandatory (the positional first argument; one segment, [a-z0-9_]+; no auto-generated loop names). the loop gets its own workspace. a loop created from inside an agent does not inherit the agent's scope: loops are global entities, not children.

names are unique among active loops. a removed loop's name is reusable; internally each definition has its own uuid, and runs and events reference it, so histories of same-named definitions never collide. loop logs <name> resolves the active definition first, else the most recent ended one; older same-named history is addressed by loop uuid (from loop ls --all --json). a once loop that has fired releases its name like any ended loop.

cadence: timezone-stored, DST-correct

the cadence is a five-field cron expression stored with the creating timezone and evaluated in it, so "9am weekdays" stays 9am across a DST boundary. each occurrence is persisted as a UTC next_fire_at. there is no --every; intervals are cron expressions (*/30 * * * *).

the alternative is --once-at <time>: a relative duration like 30m, an RFC3339 timestamp, or a local wall-clock YYYY-MM-DD[ T]HH:MM[:SS], resolved to a single UTC fire. a once loop fires once, then ends.

missed fires are skipped and logged, never replayed. if the machine slept or the server was down through a scheduled time, that fire is dropped with a log line rather than fired late. see durability and recovery.

overlap and max-concurrency

--max-concurrency caps concurrent runs of the loop (default 1). at the cap, the overlap policy decides what happens to a new fire:

  • --overlap queue (default) holds work as pending run rows. scheduled fires at capacity coalesce into at most one pending scheduled run: later fires fold into it, and its due_at becomes the earliest missed fire. manual runs always allocate their own row.
  • --overlap skip drops the fire with a log line.

when a run exits, the oldest pending run starts. pending runs are durable: they survive restarts, appear in loop run ls, and can be canceled by loop run stop before they ever start (status canceled). this is a separate axis from the agent queue and concurrency caps, which still apply to every agent a run spawns.

run statuses

every run is a durable row from the moment it is asked for, including at capacity:

pending · running · stopping · ok · failed · timeout · stopped · canceled

the command's exit maps to status: ok on exit 0, else failed; timeout when the loop's --timeout killed it; stopped for loop run stop; canceled if it was still pending when stopped or removed. loop run start always allocates the run (uuid plus run id, kind manual) and prints <loop_name>/<run_id> <run_uuid>; at capacity the run sits pending and starts when a slot frees.

process-group identity guard

a pgid alone is not proof of identity; pids get reused. so orcr records the process start time alongside pid and pgid. recovery and kills only ever signal a pgid whose start time still matches; otherwise the run is closed as dead and nothing is signaled.

stop and timeout follow one path: the run moves to a stopping barrier (new agent runs resolving to that run context are rejected from that point), then TERM to the process group, a grace period, KILL to the process group, and finally a barrier glob-kill of the run's agents (<loop>/<run_id>/**, looped until a final snapshot shows none). restart recovery is a serialized per-loop transaction: verify running rows by pgid and start-time match (dead runs are closed and their agents glob-killed), recompute the active count, honor paused and ended loops, start pending runs as slots allow, and recompute next_fire_at, skipping missed fires with an event row explaining each decision. every scheduler action (fired, coalesced, skipped, paused-hold, timed out, stopped) is an event row; that is loop logs --source orcr.

On this page