> For the complete documentation index, see [llms.txt](https://help.resouris.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.resouris.com/part-2-running-resouris/automations.md).

# Scheduled jobs

ReSouris runs a small set of scheduled background jobs. Each has a fixed daily cadence, emits audit events, and is safe to skip a run without data loss.

## Roster generation queue consumer

**Cadence:** continuous (queue-driven, not time-based). **Module:** `host/jobs/worker.py` (`process_one_job`), driven by the `bin/rostaire-worker` loop in `host/cli/worker.py` **What it does:** picks the next `roster_generation_jobs` row in state `queued` and runs the CP-SAT solver. Emits `roster.generation.started` and `roster.generation.completed` or `roster.generation.failed` per job.

## Rolling roster extension

**Cadence:** continuous (polls; acts only when a roster is behind). **Module:** `host/scheduler/roster_extension_worker.py` **What it does:** for every department with rolling mode enabled, keeps the roster at "this Monday plus eight weeks". When a department's roster falls behind that window it advances the horizon, supersedes any unauthorised draft, and queues a fresh solve. ⛔ **It proposes; it never publishes** — a human authorises the result. A missed tick simply catches up on the next one, so the poll interval is only how long a behind roster waits. See [Roster lifecycle](/how-it-works/roster-lifecycle.md#the-rolling-roster).

## Sickness batch worker

**Cadence:** continuous (queue-driven). **What it does:** consumes `roster_generation_jobs` with `kind='sickness_resolve'` — one job per sickness batch. Produces reschedule proposals per affected sub-window. See [Sickness and leave](/how-it-works/sickness-and-leave.md).

## Notification dispatch

**Cadence:** continuous (queue-driven). **Module:** `host/notifications/batch_dispatch.py` **What it does:** dispatches queued notifications via configured adapters (email, Web Push, webhook). Payload encryption for Web Push uses AES128GCM per the standard VAPID scheme.

## Audit seal

**Cadence:** daily, 02:00 UTC. **Module:** `host/audit/witness.py` **What it does:** computes a canonical summary of each tenant's chain as at end of day — chain head hash, event count, first and last event ids — takes its `sha256`, signs with the `R-AUDIT-SEAL-2026` subkey (via the configured KMS adapter), and writes to `audit_seals` and the witness backend. Days with no events are left unsealed.

The seal is what makes the audit chain **tamper-evident** — any retroactive edit changes the chain head, so the sealed summary no longer matches what the chain recomputes to. Seal-versus-witness reconciliation is scripted via `bin/rostaire-witness-verify`, which compares each seal against its off-system copy byte-for-byte and exits non-zero on mismatch. Chain verification itself is `bin/rostaire-audit-chain-verify` (daily) and `bin/rostaire-audit-verify` (on demand, per tenant); the chain-status page reports those runs.

## Backup

**Cadence:** daily, 01:00 UTC (default; configurable). **Module:** `host/scheduler/backup_consumer.py` **Adapter:** `adapters/backup_encryption/*` (cloud KMS, local PEM, or env-var provider). **What it does:** writes an encrypted database dump to the configured destination. `kind='incremental'` is currently rejected with `NOT_IMPLEMENTED` (v1.5+); `kind='full'` is supported. Emits `backup.started` / `backup.completed` / `backup.failed`.

## Licence auto-fetch (SaaS mode)

**Cadence:** daily, 03:00 UTC. **Module:** `host/licensing/renewal_cron.py` **Status:** scheduled (stubs from P22.C1); full fetcher wiring lands in v1.7 D1 per the ReSouris Licence integration plan. **What it does (when wired):** fetches the tenant's latest licence from the URL declared in the active licence body's `renewal.renewal_endpoint_url` field. If the response body sha256 matches the current one, no-op. Otherwise runs the standard `install_licence()` verification path. Emits `licence.renewal.attempted` / `succeeded` / `failed` / `skipped`.

Not applicable to self-hosted or air-gapped deployments. The env-var gate `RESOURIS_LICENCE_FETCH_ENABLED` controls activation.

## Revocation list pull

**Cadence:** daily, 03:15 UTC. **Module:** `host/licensing/revocation_pull.py` **What it does:** pulls the signed revocation list from the URL declared in the active licence body's `revocation_check_url` field. Verifies the signature against the trust anchor. Imports revocations via the standard `import-revocations` code path. Emits `ops.licence.revocation.pulled` and, per import, `licence.revocation.imported`.

Applies to all deployment modes with reachable revocation URLs. Air-gapped installs skip the pull and import revocations manually via `bin/rostaire-licence import-revocations`.

## Deep sleep guard

**Cadence:** every 5 minutes. **Module:** `host/licensing/clock_observer.py` **What it does:** watches for wall-clock drift larger than the sleep-gap threshold (default 30 minutes). Emits `ops.clock.drift_detected` if the drift exceeds threshold — a signal that the host was suspended (VM paused, laptop lid closed) and the audit-chain / licence-window checks should refuse until re-verified.

## Retention purger

**Cadence:** weekly, Sunday 04:00 UTC. **What it does:** purges soft-deleted rows past their retention window (typically 90 days for personal data, 7 years for audit events, per tenant configuration).

## Audit-chain status refresh

**Cadence:** hourly. **What it does:** rolls up per-day chain-integrity summaries into `audit_chain_status` for fast retrieval by the audit page filters (per the v1.6 UX audit-filters enhancement).

## HCS Job Plan review notifier (v1.7.4, P43)

**Cadence:** daily, 02:00 UTC. **Module:** `application/job_plan/review_notifier.py::tick`. **What it does:** walks every tenant, reads the `job_plan_review_schedule` view (MIG-195), and fires `job_plan.review_due` audit events for any active plan whose `next_review_date` falls within the pre-due window (default 30 days). Also fires on plans already overdue so the notifier nags on missed reviews. Idempotent per (plan, next\_review\_date) via a 45-day audit-event dedupe check — multi-day outages produce at most one event per (plan, next\_review\_date), not one per day. Payload includes the resolved line manager's `staff_id` per MIG-171/185 so downstream consumers (email routing, dashboard cards) don't have to re-query.

## Quarterly reconciliation snapshotter (v1.7.4, P43)

**Cadence:** daily (idempotent; effectively refreshes the current quarter's snapshots when they haven't been signed off yet). **Module:** `application/job_plan/reconciler.py::tick` + `snapshot_quarter_for_tenant`. **What it does:** walks every tenant, computes the current quarter's planned-vs-delivered rollup per active `job_plan`, and writes / refreshes rows in `job_plan_quarterly_reconciliation` (MIG-204). Snapshots are **immutable-once-signed** — the `DO UPDATE` clause is guarded by `WHERE signed_utc IS NULL`, so a signed snapshot is never mutated by the reconciler. Trust Admins sign off via the reconciliation UI at `/ui/admin/job-plan-reconciliation/{year}/{quarter}/sign` (requires `admin.job_plan.reconciliation.signoff`). See [Quarterly workload plan reconciliation](/job-planning/quarterly-reconciliation.md).

## Runtime SPA drift detector (v1.7.4, P43)

**Cadence:** weekly, Sunday 04:00 UTC. **Module:** `application/job_plan/spa_check.py::tick`. **What it does:** for every active job plan, recomputes the actual-vs-planned SPA percentage from delivered clinical activity + the current entitlement snapshot. Emits `job_plan.spa_drift_detected` audit events for plans whose drift exceeds the Trust-configurable threshold. Idempotent via 7-day dedupe. The event is a compliance signal only — v1.7.4 does not block plan operation on drift; it surfaces the fact.

## Appeal SLA breach notifier (v1.7.4, P43)

**Cadence:** daily, 03:00 UTC. **Module:** `application/job_plan/appeal_sla.py::tick`. **What it does:** walks every tenant, reads open `job_plan_appeal` rows (MIG-201, `closed_utc IS NULL`), compares age against per-kind SLA (Guidelines §6.11: informal 7d, mediation 14d, formal\_appeal 28d, executive\_review 42d — monotonically-non-decreasing by cascade severity), and fires `job_plan.appeal_sla_breached` audit events for over-SLA appeals. Idempotent per (tenant, appeal) via a **1-day dedupe window** — daily cron means same-day re-fires would spam the audit trail without adding information; a still-open breach re-fires the next day (and every day thereafter) as a live nag signal until the appeal is resolved. See [Consistency panels + enhanced scrutiny + appeal SLA](/job-planning/consistency-and-appeal-sla.md).

***

**Where to configure cadences:** most jobs read their cron from `deployment_settings` (per-tenant) or env vars. See the specific module docstring for the exact knob. Skipping a run is safe; the next run picks up any missed work idempotently.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://help.resouris.com/part-2-running-resouris/automations.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
