> ## Documentation Index
> Fetch the complete documentation index at: https://opensre.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Scheduled Deliveries (Cron)

> Cron-driven recurring reports to Telegram, Slack, Discord, Rocket.Chat, and the interactive shell

# 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

```bash theme={null}
# Add a daily summary to Telegram at 09:00 IST on weekdays
opensre cron add --kind daily_summary --cron "0 9 * * 1-5" \
  --name "Morning report" --tz Asia/Kolkata --provider telegram --chat-id <chat_id>

# Same kind to Slack via incoming webhook (omit --chat-id; webhook is channel-bound)
opensre cron add --kind daily_summary --cron "0 9 * * 1-5" \
  --tz Europe/London --provider slack

# Slack bot token path still needs an explicit channel
opensre cron add --kind daily_summary --cron "0 9 * * 1-5" \
  --tz Europe/London --provider slack --chat-id C0123ABCD

# GitHub PR sweep standup digest → Slack
opensre cron add --kind github_pr_sweep --cron "0 9 * * 1-5" \
  --tz Europe/London --provider slack --chat-id C0123ABCD

# Local interactive-shell inbox delivery, useful for debugging
opensre cron add --kind daily_summary --cron "0 9 * * 1-5" \
  --name "Local morning report" --provider interactive_shell

# List configured tasks
opensre cron list

# Run a task immediately (for debugging)
opensre cron run <task_id>

# Start the scheduler daemon (or rely on `opensre gateway start`)
opensre cron start
```

## CLI Commands

### `opensre cron add`

Create a new scheduled delivery task.

| Option       | Required | Description                                                                                                                                  |
| ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `--name`     | No       | Human-readable loop name shown by `opensre cron list` and `/loops`                                                                           |
| `--kind`     | Yes      | Task kind: `daily_summary`, `weekly_audit`, `incident_window_replay`, `synthetic_run`, `custom_investigation`, `github_pr_sweep`             |
| `--cron`     | Yes      | Cron expression (5 fields: minute hour day month day\_of\_week)                                                                              |
| `--tz`       | No       | IANA timezone (default: `UTC`). Examples: `Europe/London`, `US/Eastern`, `Asia/Kolkata`                                                      |
| `--provider` | Yes      | Messaging provider: `telegram`, `slack`, `discord`, `rocketchat`, `interactive_shell`                                                        |
| `--chat-id`  | Cond.    | Required for telegram/discord/rocketchat. Optional for slack when an incoming webhook is configured, and not needed for `interactive_shell`. |
| `--window`   | No       | Lookback window in hours (default: `24`)                                                                                                     |

### `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:

```
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Mon-Sun)
│ │ │ │ │
* * * * *
```

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 variables** — `TELEGRAM_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

| Kind                     | Behavior                                                                                                                                         |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `daily_summary`          | Runs the investigation pipeline with a daily summary source. Falls back to "no incidents" on empty result, or "pipeline unavailable" on failure. |
| `weekly_audit`           | Runs the pipeline with a weekly audit source. Same fallback behavior.                                                                            |
| `incident_window_replay` | Replays the investigation pipeline over the configured window. Raises on failure (operator should know).                                         |
| `synthetic_run`          | Runs the pipeline with a synthetic source. Raises on failure.                                                                                    |
| `custom_investigation`   | Runs a custom investigation with user-provided params. Credential keys are stripped before forwarding to the pipeline.                           |
| `github_pr_sweep`        | Headless GitHub PR standup (mergeable / stale / conflicted). Requires GitHub configured; posts via the chosen provider.                          |

Sentry morning digests use a separate CLI (`opensre sentry digest schedule …`),
not `opensre cron add`. See [Sentry](/docs/sentry#morning-digest-scheduled).

## 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:

```text theme={null}
/loops add --name "Morning ops" --time 08:30 --prompt "Check open incidents and summarize production risk" --run-now
/loops next <loop_id>
/loops stop <loop_id>
/loops delete <loop_id>
/loops messages
```

You can also ask naturally:

```text theme={null}
Set up a manual loop called Morning ops at 08:30 UTC to check open incidents and summarize production risk, and run it once now.
```

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:

```
/cron list
/cron add --kind daily_summary --cron "0 9 * * *" --provider telegram --chat-id <id>
/cron run <task_id>
```

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.
