# Scheduled and cron job delivery Priority2 uses durable delivery claims and attempt history. Apply migration 20 (`migrations/models/20_20261012231017_application_foundation.py`) before running the updated backend. It adds `job_deliveries` and `job_attempts`, unique delivery identity `(kind, job_id, run_id)`, claim tokens/timestamps, scheduled-message `series_id`, unique `previous_occurrence_id`, `deleted_at`, and nullable cron `created_by_id`. Migration 20 does not assign owners to existing cron jobs. Downgrading drops delivery history and ownership/series fields. ## Scheduled messages The panel preserves creation, cancellation and deletion. It polls the message list every ten seconds and exposes list/action errors, including permission or claim conflicts. Delivery history is loaded on demand with authenticated `GET /api/scheduled-messages//deliveries`. Refresh history to see later changes; a retry refreshes both the list and any open history, even when it fails. History includes each delivery's run ID, status/error, and every attempt's status, error, start time and finish time. An unfinished attempt is explicitly labelled. | Status | Meaning / action | | --- | --- | | `pending` | Waiting for its scheduled time; cancel or delete is available. | | `processing` | A worker claimed the delivery; retry is unavailable. | | `sending` | Attempt/history status: provider call is underway. | | `sent` | Provider acknowledged delivery; retry is unavailable. | | `failed` | Failure known to be safe to retry; explicit retry is available only when `retryable: true`. | | `uncertain` | Delivery might already have happened. Reconcile with provider records or the recipient before considering any resend. | | `cancelled` | Future work cancelled; delete is available. | Retry uses authenticated `POST /api/scheduled-messages//retry` with no schedule or status mutation. It executes the existing delivery identity and records a new attempt. A retry can return HTTP 409 with a changed message status (including `uncertain`); the UI surfaces the error and reloads the state. Editing a failed message's scheduled time does not retry it. The frontend update service accepts only `cancelled` as a status mutation and rejects `pending` at runtime, so updating status cannot bypass the explicit retry endpoint. Uncertain outcomes include ambiguous provider failures and abandoned claims. They are never automatically retried. This panel has no reconciliation or force resend action. Resolve the evidence through an operational reconciliation process; do not create a replacement message or new run ID simply to bypass uncertainty. Recurring occurrences have separate rows and delivery identities, linked by `series_id` and `previous_occurrence_id`. Cancelling a recurring occurrence cancels the series' future work. The panel labels this action **Cancel series**, including on completed or uncertain occurrences where future work must be stopped. Already started provider calls may still complete; cancellation cannot recall a delivered message, and attempt history remains evidence of the outcome. Deleting a pending/cancelled recurring occurrence uses the backend's soft delete and durable cancellation tombstone: the selected occurrence is hidden, and future series work is cancelled so recurrence repair cannot revive it. Other cancelled occurrences can remain visible. Nonrecurring pending/cancelled rows use the existing delete API. The UI never simulates cancellation/deletion by altering local series rows; it reloads the backend result. ## Cron callers and ownership `POST /api/cron//trigger` requires `X-Cron-Secret` and a nonblank run identifier, maximum 255 characters. Supply either a JSON `run_id` or an `Idempotency-Key` header; the header takes precedence. Choose a stable identifier for each logical execution (for example the cron occurrence's UTC timestamp), and reuse it across repeated trigger requests for that execution. A new logical occurrence needs a new ID. Repeating a trigger with the same ID returns the existing outcome without resending; it does not retry a failed or uncertain delivery. ```http POST /api/cron//trigger X-Cron-Secret: Idempotency-Key: daily-report:2026-10-13T12:00:00Z Content-Type: application/json {} ``` Legacy cron jobs require an explicitly assigned existing user. An administrator must use authenticated `PUT /api/cron/` with `{"created_by":""}` after reviewing the correct owner. Execution fails safely until ownership is assigned; it never selects an arbitrary admin. New jobs get the authenticated creator as owner. Agent execution uses that owner's context and permissions. Assigning ownership does not itself retry an earlier failed execution. Authenticated `GET /api/cron//deliveries` returns the same history structure. For a safe failed execution only, an administrator can use authenticated `POST /api/cron//retry` with `{"run_id":""}`. Reuse the failed run's ID; do not invent an ID to evade delivery claims. Processing, sending, sent, cancelled and uncertain executions are ineligible. Uncertain cron deliveries need the same reconciliation as scheduled messages. Database claims prevent duplicate workers from winning the same delivery; they cannot prove exactly-once external delivery when a provider acknowledgement is lost. ## Frontend validation No additional packages are required. Tests use Node's test runner, the installed TypeScript transpiler and React's static renderer, following the existing renderer harness in `raggr-frontend/tests/history-management.test.cjs`. ```sh cd raggr-frontend node --test tests/job-delivery*.cjs ./node_modules/.bin/tsc --noEmit --target ES2020 --lib DOM,ES2020 ```