Scheduled Deliveries
OpenSRE can deliver recurring reports to messaging providers on a cron schedule. Use it for recurring prompt loops, repository-scoped GitHub CI health reports, GitHub PR standups, PostHog metric reports, and work-item reminders — delivered automatically without manual CLI runs. For human task reminders and proactive work check-ins, useopensre work add --remind-at ... or opensre work schedule-checkin .... Those commands create
the right scheduler entries, support repeated --target provider:chat_id fan-out
delivery, and keep task metadata in the work-item store.
Demo picker on launch
Every interactive launch (opensre) asks which demo to run. The recommended option analyzes CI/CD performance on a real repository. Option B schedules a CI/CD reliability agent; see CI/CD analytics demo. Escape skips onboarding; “Or type your own answer…” lets you submit a different request. Type /demo to start again.
Quick Start
CLI Commands
opensre cron add
Create a new scheduled delivery task.
opensre cron list
Display all configured scheduled tasks in a table, including the loop name, enabled state, next cron fire time, and last run.
opensre cron remove <task_id>
Remove a scheduled task by its ID. Its run history remains available through
opensre cron logs <task_id>.
opensre cron status
Show the number of durable runs waiting for execution, including expired claims
awaiting recovery. Age starts at admission for pending runs and at lease expiry
for interrupted runs; live claims are excluded. Waiting work remains visible
across scheduler restarts and while a task is paused or disabled. Deleted tasks
are excluded. Use --json for structured
output suitable for monitoring and runbooks. JSON output includes status: ok;
if the task store or run database cannot be read (including storage access or
lock failures), the command exits unsuccessfully and reports status: unknown
with null metrics instead of returning a misleading empty backlog. The JSON
error field identifies task_store_unreadable or run_store_unreadable.
opensre cron run <task_id>
Execute a task immediately (ad-hoc one-shot). Useful for debugging delivery
without waiting for the next cron tick.
By default this delivers to every configured destination again, even ones that
already received the message — that is what you want to trigger a task on
demand. To recover a partial failure instead, use --failed-only: it retries
only the destinations the most recent run failed at, using the saved report
without executing the task again. A missing saved report requires a deliberate
full run.
--failed-only never widens. If it cannot read per-target history for the task
— no prior run, or one recorded before that history was tracked — it stops and
tells you, rather than quietly delivering everywhere. Re-run without the flag
when a full send is what you want.
When the last run was a partial failure, a plain cron run warns which
destinations it is about to re-deliver to before it sends.
opensre cron logs <task_id>
Show execution history and the newest retained report. Work status and delivery
status are separate: blocked work can have a successfully delivered report.
unknown work status means the older run did not retain completion evidence.
Use --run <run_id> to read one historical attempt and --json for structured
results. Reports remain available after removing the schedule.
For a repository repair loop, use --kind manual_loop --mode agent with
--owner <owner> --repo <repo> and optionally --pr <number> or
--branch <branch>. The repository target is saved with the task. CI repairs
and security repairs that open a PR create their own matching checkouts when
no explicit workspace is supplied.
opensre cron run <task_id> prints the work result, delivery result, and report.
A blocked or incomplete agent task exits unsuccessfully even if its report was
delivered. Agent tasks need a structured completion result from their execution
tools; report text alone is recorded as unverified work.
opensre cron start
Start the blocking scheduler daemon. Loads all enabled tasks and fires them
according to their cron schedules. Blocks until SIGINT or SIGTERM.
To keep it running without a terminal, /loops service install in the shell
registers a per-user service (launchd on macOS, systemd on Linux) that runs
opensre cron start --service at login; /loops service remove deletes it.
Cron Syntax
Standard 5-field cron expressions:CronTrigger, where
0 = Monday and 6 = Sunday (not the Unix cron convention where
0 is Sunday). Prefer 1-5 for weekdays to avoid confusion.
Examples:
0 9 * * 1-5— weekdays at 09:000 8 * * 1— Mondays at 08:00*/30 * * * *— every 30 minutes0 0 1 * *— first day of each month at midnight
0-59) to get a six-field expression:
*/30 * * * * *— every 30 seconds0 */5 * * * *— every 5 minutes, on the minute (same as*/5 * * * *)
cron add time using APScheduler’s
CronTrigger. Invalid expressions are rejected immediately.
Timezone Behavior
- All fire times are internally converted to UTC for dedup consistency
- The
--tzoption accepts any IANA timezone (for exampleEurope/London,US/Eastern) - DST transitions are handled correctly — the UTC-normalized dedup key ensures no duplicate or missed deliveries across clock changes
Dedup Semantics
The scheduler uses a SQLite-backed claim store with a unique task, fire-time, and attempt key. Each attempt has a 30-minute lease and a random owner token:- When a cron tick fires,
EVENT_JOB_SUBMITTEDcapturesscheduled_run_times[0]and the job uses that UTC-normalizedfire_timefor the claim key - The executor creates an attempt with a 30-minute lease and owner token
- If the current attempt is still leased, another instance skips the tick
- If the lease expired, the old attempt is marked
abandonedand one new worker creates the next attempt - Completion is accepted only from the attempt’s owner token, so an old worker cannot overwrite a reclaimed attempt
--failed-only retry, so it does not resend to destinations
outside that retry’s scope.
Attempts created before delivery scopes were persisted cannot safely recover
automatically. They appear as failed runs with a delivery-scope error; use
opensre cron run <task-id> --failed-only when readable per-target history exists,
or explicitly rerun the task for all destinations after checking its logs.
This prevents concurrent duplicate claims and recovers ticks after worker
crashes. A crash after an external provider accepts a message but before the
completion is recorded can still result in a duplicate on reclaim; preventing
that case requires provider-specific idempotency support.
Capacity and overload
Each admitted cron tick is first written as a durablepending row in
scheduler.db. It is not silently dropped because workers are busy. The
default scheduler concurrency remains 2; a scheduled agent turn also needs
a permit from the shared process turn gate, so the effective agent-turn
concurrency can be lower.
The current scheduler bounds active workers, but its executor can still retain
more submitted callbacks in memory while those workers are busy. A large burst
therefore remains recoverable from SQLite, but is not yet a bounded in-memory
queue. Use the baseline below to size an installation rather than raising the
default to absorb a burst. A later scheduler change will move overflow to the
durable pending store and reserve recovery control work from user execution.
The capacity baseline records these terms consistently:
- Durable pending count and oldest pending age:
pendingrows and the elapsed time since their durable admission. The benchmark reads this directly untilopensre cron statusexposes it. - In-memory callbacks: callbacks submitted to the current worker executor that have not completed. This is the current overload signal, not a durable queue limit.
- Throughput and drain time: successful fake deliveries per second and the elapsed time to finish admitted work.
- Recovery query latency and plan: time and SQLite plan used to find pending or expired work. Completed history must not expand the candidate scan.
- SQLite lock failures and RSS: lock errors plus current and peak process resident memory sampled by the benchmark host.
OPENSRE_SCHEDULER_MAX_CONCURRENT_RUNS:
docs/benchmarks/scheduler-capacity-baseline.json records the exact workload,
host metadata, SQLite version, and observed values. Restart recovery uses the
production shape: one recovery callback iterates its durable candidates
serially.
Credential Resolution
Credentials are resolved lazily at delivery time in this priority order:- Task params — credentials stored in the task definition (not recommended)
- Integration store —
~/.opensre/integrations.json(configured viaopensre integrations) - Environment variables —
TELEGRAM_BOT_TOKEN,SLACK_BOT_TOKEN,DISCORD_BOT_TOKEN,ROCKETCHAT_*
cron add time — they are picked up
from your existing integration configuration.
Task Kinds
Sentry morning digests use a separate CLI (
opensre sentry digest schedule …),
not opensre cron add. See Sentry.
Persistence
- Task definitions are stored in
~/.opensre/scheduler_tasks.json(JSON + filelock) - Execution history is stored in
~/.opensre/scheduler.db(SQLite with WAL mode) - Interactive-shell loop messages are stored in
~/.opensre/scheduler_loop_messages.jsonl - Operational breadcrumbs are appended to
~/.opensre/operations_log.jsonlwith loop lifecycle and run status metadata, not prompt or message bodies - Both survive process restarts —
opensre cron listandopensre cron logsread from disk
OPENSRE_OPERATIONS_LOG_PATH to write the operations log somewhere else, or
OPENSRE_OPERATIONS_LOG_DISABLED=1 to turn it off. Set
OPENSRE_OPERATIONS_LOG_MAX_BYTES to change the rotation threshold.
REPL
Prefer/loops for user-facing recurring prompt loops:
/loops add sends to every configured default handle it can reach: Telegram when TELEGRAM_BOT_TOKEN and TELEGRAM_DEFAULT_CHAT_ID are configured, Slack when SLACK_WEBHOOK_URL is configured or when SLACK_BOT_TOKEN and SLACK_DEFAULT_CHAT_ID are configured, and the local interactive-shell inbox.
When a scheduled loop or skill finishes, the scheduler delivers the report body to those channels without you prompting — you do not need to be in the REPL or Slack chat at fire time. Loops created before Slack was in your default channel list stay inbox-only until you recreate them with Slack in --channels (or rely on the updated defaults for new loops).
The lower-level /cron slash command forwards to the CLI:
/loops to see named loops with active/draft state, execution time, channels, last run, and next fire time. Use /loops stop <loop_id> to pause a loop without losing it, /loops start <loop_id> to re-enable it, and /loops delete <loop_id> to remove it. Onboarding seeds a few draft starter loops, such as a weekday morning report, so you have concrete examples before creating an active delivery.