Skip to main content

Scheduled Deliveries

OpenSRE can deliver recurring reports to messaging providers on a cron schedule. This enables daily reliability digests, weekly alert audits, synthetic test summaries, and custom investigations — all delivered automatically without manual CLI invocations. For human task reminders and proactive work check-ins, use opensre 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.

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>

Delete a scheduled task by its ID.

opensre cron run <task_id>

Execute a task immediately (ad-hoc one-shot). Useful for debugging delivery without waiting for the next cron tick.

opensre cron logs <task_id>

Show execution history for a task (newest first). Displays start time, status, message ID, and any errors.

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.

Cron Syntax

Standard 5-field cron expressions:
Examples:
  • 0 9 * * 1-5 — weekdays at 09:00
  • 0 8 * * 1 — Mondays at 08:00
  • */30 * * * * — every 30 minutes
  • 0 0 1 * * — first day of each month at midnight
Cron expressions are validated at 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 --tz option accepts any IANA timezone (e.g., Europe/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_id, fire_time) constraint:
  1. When a cron tick fires, EVENT_JOB_SUBMITTED captures scheduled_run_times[0] and the job uses that UTC-normalized fire_time for the claim key
  2. The executor attempts an INSERT OR IGNORE into the claim table
  3. If the insert succeeds (rowcount = 1), this instance won the claim and delivers
  4. If the insert is ignored (rowcount = 0), another instance already claimed it — skip
This ensures exactly-once delivery even when multiple scheduler instances run concurrently (e.g., on a laptop and a hosted process).

Credential Resolution

Credentials are resolved lazily at delivery time in this priority order:
  1. Task params — credentials stored in the task definition (not recommended)
  2. Integration store~/.opensre/integrations.json (configured via opensre integrations)
  3. Environment variablesTELEGRAM_BOT_TOKEN, SLACK_BOT_TOKEN, DISCORD_BOT_TOKEN, ROCKETCHAT_*
This means you don’t need to pass credentials at cron add time — they’re 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
  • Both survive process restarts — opensre cron list and opensre cron logs read from disk

REPL

Prefer /loops for user-facing recurring prompt loops:
You can also ask naturally:
By default, /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, and the local interactive-shell inbox. The lower-level /cron slash command forwards to the CLI:
Run /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.