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

# Cron Jobs

> System-only scheduled maintenance and automation endpoints. Secured with CRON_SECRET / CRON_TOKEN. Intended caller is AWS EventBridge or an equivalent external scheduler.

<Note>
  These endpoints are **system-only**. They are authenticated with a shared
  secret: every `/api/v1/cron/*` route accepts `CRON_SECRET` **or** the legacy
  `CRON_TOKEN` alias (whichever is set), and the legacy `/api/cron/tick` accepts
  either as well. They are intended to be invoked by the in-process maintenance
  scheduler, AWS EventBridge Scheduler, or an equivalent external cron.

  During pilot / initial deployment, EventBridge is typically **not** wired.
  That means:

  * `/api/cron/tick` exists and is callable, but is effectively **unused**
    until a scheduler starts hitting it.
  * Alert rules and report schedules will not fire automatically until a
    caller invokes this endpoint.
  * Features like **Run Now** (reports) and on-demand alert evaluation work
    independently and do not require any cron wiring.

  See [EventBridge Setup](../self-host/deploy/eventbridge-setup) for the setup
  path (marked optional on purpose).
</Note>

## GET /api/v1/cron/export-interactions

Exports new interaction history rows for all active tenants as Hive-partitioned NDJSON files. Uses checkpoint tracking to export only rows created since the last run.

Intended to be called by a cron scheduler (e.g., Vercel Cron Jobs, Kubernetes CronJob). Processes up to 10,000 rows per tenant per invocation.

### Authentication

Requires a `CRON_SECRET` token via either:

* `Authorization: Bearer <CRON_SECRET>` header
* `X-Cron-Secret: <CRON_SECRET>` header

### File Layout

```
exports/{tenantId}/interaction_history/year=YYYY/month=MM/day=DD/batch-{timestamp}.json
```

Each file contains newline-delimited JSON (NDJSON), compatible with Hive, Spark, Athena, and other data lake tools.

### Response

```json theme={null}
{
  "ok": true,
  "totalExported": 8500,
  "tenants": [
    {
      "tenantId": "tenant_001",
      "tenantName": "Acme Corp",
      "exported": 5200,
      "filePath": "exports/tenant_001/interaction_history/year=2026/month=03/day=30/batch-2026-03-30T02-00-00-000Z.json"
    },
    {
      "tenantId": "tenant_002",
      "tenantName": "Beta Inc",
      "exported": 3300,
      "filePath": "exports/tenant_002/interaction_history/year=2026/month=03/day=30/batch-2026-03-30T02-00-01-000Z.json"
    }
  ]
}
```

### Error Handling

If export fails for one tenant, other tenants continue processing. Failed tenants include an `error` field and their checkpoint is marked as `failed` for retry on the next run.

<Note>
  In production, replace the local filesystem writes with S3 uploads via `@aws-sdk/client-s3`. The file paths follow the same Hive-style partitioning scheme.
</Note>

***

## GET /api/v1/cron/cleanup

Periodic data cleanup that removes expired suppressions, purges interaction history, interaction summaries, and decision traces according to each tenant's retention configuration, and ensures future database partitions exist.

### Authentication

Requires `CRON_SECRET` via `Authorization: Bearer <secret>` or `X-Cron-Secret` header. If `CRON_SECRET` is not set in the environment, the endpoint is **fail-closed** and rejects every request with 401.

### Cleanup Operations

| Operation                   | Description                                                                                                                                                                                                                                                                                                                                                                          |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Expired suppressions        | Deletes any suppression entry whose expiry timestamp is in the past. Event-driven; does not consult per-tenant retention settings.                                                                                                                                                                                                                                                   |
| Per-tenant history purge    | For every tenant, applies that tenant's retention settings — history days, summary days, decision-trace days — together with the `legalHold` flag. Purges interaction history, interaction summary rows (all four period types — daily, weekly, monthly, all-time), and decision-trace rows. A tenant with `legalHold = true` is skipped per-table and surfaced in `legalHoldSkips`. |
| Expired variant assignments | Per tenant, deletes experiment variant-assignment rows past their expiry, counted in `expiredVariantAssignments`.                                                                                                                                                                                                                                                                    |
| Aged audit logs             | Per tenant, purges audit-log rows older than the retention window, counted in `oldAuditLogs`.                                                                                                                                                                                                                                                                                        |
| Published outbox events     | Per tenant, removes already-published outbox rows, counted in `publishedOutboxEvents`.                                                                                                                                                                                                                                                                                               |
| Partition maintenance       | Creates database partitions for the next 3 months (non-fatal if partitions are not configured).                                                                                                                                                                                                                                                                                      |

A failure inside one tenant's purge is captured in `perTenantErrors` and does not abort the run — the cron will continue with the remaining tenants and still emit a 200 response.

### Response

```json theme={null}
{
  "status": "ok",
  "cleaned": {
    "expiredSuppressions": 45,
    "tenantsProcessed": 12,
    "totalHistoryPurged": 8400,
    "totalSummariesPurged": 1200,
    "totalDecisionsPurged": 600,
    "legalHoldSkips": {
      "tenant-uuid-1": ["interactions", "decisions"]
    },
    "partitionsEnsured": 1,
    "expiredVariantAssignments": 30,
    "oldAuditLogs": 512,
    "publishedOutboxEvents": 1840,
    "perTenantErrors": []
  },
  "timestamp": "2026-04-27T02:00:00.000Z"
}
```

### Error Codes

| Code  | Reason                          |
| ----- | ------------------------------- |
| `401` | Invalid or missing CRON\_SECRET |
| `500` | Cleanup operation failed        |

***

## GET /api/v1/cron/staging-janitor

Reaps leaked `_flow_*` staging tables — the per-run source/transform/branch
scratch tables the pipeline runtime materializes. Tables belonging to
in-flight runs are **never** dropped; only orphaned tables (the run row was
deleted, or the run finished before the retention cutoff) are removed. Tables
matching the `_flow_*` prefix but not the expected shape are counted as
`unparseable` and left in place.

### Authentication

Requires `CRON_SECRET` (or the legacy `CRON_TOKEN` alias) via
`Authorization: Bearer <secret>`. Fail-closed with `401` when neither is set.

### Configuration

| Env var                        | Default | Description                                                                                                       |
| ------------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------- |
| `FLOW_STAGING_RETENTION_HOURS` | `24`    | Staging tables for finished runs older than this window are dropped. In-flight runs are exempt regardless of age. |

### Response

```json theme={null}
{
  "ok": true,
  "scanned": 42,
  "dropped": 7,
  "kept": 33,
  "unparseable": 2,
  "errors": [],
  "droppedTables": ["_flow_xform_abc123_node4", "_flow_branch_abc123_b1_d2"]
}
```

`droppedTables` is capped at the first 50 entries in the response; the full
list is written to the log.

### Error Codes

| Code  | Reason                                        |
| ----- | --------------------------------------------- |
| `401` | Invalid or missing CRON\_SECRET / CRON\_TOKEN |
| `500` | Janitor run failed                            |

<Note>
  Invoked hourly by the in-process maintenance scheduler when a cron secret is
  set (see [Cron tier](/self-host/configure/cron-tier)), and available to any
  external scheduler (EventBridge, Kubernetes CronJob).
</Note>

***

## GET /api/v1/cron/approvals-expire

Sweeps every pending approval request and flips any that has aged past
`APPROVAL_MAX_AGE_HOURS` (default 168 hours = 7 days) to `status = "expired"`.
The operation is a single bulk database write and is fully idempotent — running
it again immediately is a no-op because no rows match the cutoff anymore.

### Authentication

Same `CRON_SECRET` shared-header pattern as `/api/v1/cron/cleanup`:

* `Authorization: Bearer <CRON_SECRET>` header
* `X-Cron-Secret: <CRON_SECRET>` header

Unset `CRON_SECRET` → the endpoint rejects all requests with 401 (fail-closed).

### Configuration

| Env var                  | Required | Description                                                                                                                   |
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `CRON_SECRET`            | Yes      | Shared secret. Fail-closed when unset.                                                                                        |
| `APPROVAL_MAX_AGE_HOURS` | No       | Maximum age in hours before a pending approval auto-expires. Default `168` (7 days). Invalid values fall back to the default. |

### Recommended schedule

Every 15 minutes is plenty — the operation is O(rows-to-expire) at the
database with zero per-row work in Node. Once nothing is left to expire,
each invocation is essentially free.

### Response

```json theme={null}
{
  "status": "ok",
  "maxAgeHours": 168,
  "cutoff": "2026-04-16T17:00:00.000Z",
  "expired": 7,
  "durationMs": 23,
  "timestamp": "2026-04-23T17:00:00.000Z"
}
```

### Error codes

| Code  | Reason                                                |
| ----- | ----------------------------------------------------- |
| `401` | Missing or invalid `CRON_SECRET`                      |
| `500` | Underlying database write failed (transient DB issue) |

***

## GET /api/v1/cron/ai-autopilot

Decisioning **Autopilot** sweep. For every tenant it gathers experiment-promotion and model-drift
proposals, persists them as `AiRecommendation` rows (`source: "autopilot"`), then acts according
to the tenant's [`aiAutopilot.mode`](/api-reference/tenant-settings#ai-autopilot-settings):

* `suggest` — inbox only (proposals created, nothing applied).
* `auto_gated` — opens an [ApprovalRequest](./approvals) for each proposal.
* `auto` — applies the change and writes an audit entry.

### Authentication

`CRON_SECRET` via `Authorization: Bearer <secret>` or `X-Cron-Secret: <secret>`. Fail-closed with
`401` when `CRON_SECRET` is unset.

### Response

```json theme={null}
{
  "ok": true,
  "tenants": 12,
  "proposalsCreated": 34,
  "applied": 8,
  "approvalsOpened": 5,
  "errors": 0,
  "summaries": []
}
```

***

## GET /api/v1/cron/ai-sentinel

Decision **Sentinel** sweep (recommended every 30 min). Computes `suppression_rate` and
`empty_candidate_rate` per tenant, writes [System Health](/api-reference/system-health) alerts on
breach, and — only for tenants with `aiAutopilot.sentinelAutoPause = true` — pauses active flows on
a hard breach.

### Authentication

`CRON_SECRET` via `Authorization: Bearer <secret>` or `X-Cron-Secret: <secret>`. Fail-closed with
`401` when `CRON_SECRET` is unset.

### Response

```json theme={null}
{
  "ok": true,
  "tenants": 12,
  "breaches": 2,
  "alertsCreated": 2,
  "flowsPaused": 1,
  "summaries": []
}
```

***

## Complete cron job catalog

Every `/api/v1/cron/*` job uses the same `CRON_SECRET` (or legacy `CRON_TOKEN`) bearer auth and
fails closed when the secret is unset. Jobs documented in full above are cross-linked; the rest
share the same auth and an `{ "ok": true, ... }` response shape.

| Job                              | Verb     | Purpose                                                                                          |
| -------------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `ai-autopilot`                   | GET      | Autopilot proposal sweep (documented above).                                                     |
| `ai-sentinel`                    | GET      | Decision Sentinel breach sweep (documented above).                                               |
| `approvals-expire`               | GET      | Expire pending approvals past `APPROVAL_MAX_AGE_HOURS` (documented above).                       |
| `backfill-direction-adaptations` | POST     | One-shot backfill of inbound/outbound `model_adaptations` from historical `interaction_history`. |
| `cleanup`                        | GET      | Retention purge of history/summaries/traces + partition maintenance (documented above).          |
| `drain-queues`                   | GET/POST | Drain the BullMQ queues with ephemeral workers (see [Drain Queues](./cron-drain-queues)).        |
| `dsar-purge`                     | GET      | Purge tenant data past the `RetentionConfig` window (DSAR / right-to-erasure).                   |
| `engagement-health-recompute`    | GET      | Nightly recompute of `CustomerEngagementHealth` from the last 90 days.                           |
| `export-interactions`            | GET      | Hive-partitioned NDJSON export of new interaction history (documented above).                    |
| `fairness-recheck`               | GET      | Re-run the fairness gate for tenants with continuous recheck; auto-pause on drift.               |
| `flow-scheduler-tick`            | GET      | Dispatch due schedule- and file-arrival-triggered pipelines (also runs in-process).              |
| `gitops-drift-check`             | GET      | Nightly compare of live resources vs the last-applied GitOps YAML snapshot.                      |
| `outbox-reaper`                  | GET      | Reset `outbox_events` rows stuck in `processing` past the staleness window back to `pending`.    |
| `scheduled-retrains`             | GET      | Trigger due model retrains and apply evidence decay to adaptations.                              |
| `siem-ship`                      | GET      | Push recent `AuditLog` rows to the configured SIEM.                                              |
| `staging-janitor`                | GET      | Reap orphaned `_flow_*` staging tables (documented above).                                       |
| `system-health-purge`            | GET      | Delete `SystemHealthAlert` rows past retention (`pinned` rows are kept).                         |

***

## POST /api/cron/tick

System cron endpoint that evaluates every enabled [alert rule](./alerts)
and fires every due [report schedule](/operations-reporting/report-schedules) for every
tenant in a single pass.

The intended automated caller is AWS EventBridge, invoking the endpoint at
whatever cadence matches your `windowMinutes` values (every 1–5 minutes is
typical). See [EventBridge Setup](../self-host/deploy/eventbridge-setup) for the
wiring path.

<Note>
  **Pilot deployments:** During pilot / initial rollout, EventBridge is
  typically not wired. The endpoint still exists and can be invoked on
  demand via `curl` with a valid `CRON_TOKEN` — useful for development,
  smoke testing, or one-off evaluation pushes. Without an external caller
  hitting it on a cadence, alerts and scheduled reports do not fire
  automatically; use the UI's **Run Now** / **Test** buttons or the
  per-entity `/run-now` APIs for immediate delivery.
</Note>

### Authentication

Requires a `CRON_TOKEN` via either:

* `x-cron-token: <CRON_TOKEN>` header (preferred)
* `x-cron-secret: <CRON_TOKEN>` header (legacy, same value)
* `Authorization: Bearer <CRON_TOKEN>` header

Unset `CRON_TOKEN` → the endpoint rejects all requests with 401 (fail-closed).

### Example

```bash theme={null}
curl -X POST https://your-deployment/api/cron/tick \
  -H "x-cron-token: $CRON_TOKEN"
```

### Response

```json theme={null}
{
  "ok": true,
  "tenantsProcessed": 3,
  "rulesEvaluated": 12,
  "rulesFired": 2,
  "errors": [],
  "durationMs": 187,
  "timestamp": "2026-04-17T12:34:56.789Z"
}
```

### Error Isolation

Per-tenant and per-rule errors are caught and reported in the `errors` array;
a failing tenant does not abort the tick for other tenants:

```json theme={null}
{
  "ok": false,
  "tenantsProcessed": 2,
  "rulesEvaluated": 7,
  "rulesFired": 1,
  "errors": [
    { "tenantId": "tenant-c", "error": "evaluator threw: pg offline" }
  ],
  "durationMs": 245,
  "timestamp": "2026-04-17T12:34:56.789Z"
}
```

`ok` is `true` only when `errors` is empty.

***

## Environment Variables

| Variable                 | Required                                      | Description                                                                                                                                                        |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `CRON_SECRET`            | Recommended                                   | Shared secret for `/api/v1/cron/*` maintenance endpoints                                                                                                           |
| `CRON_TOKEN`             | Required *only* when `/api/cron/tick` is used | Shared secret for `/api/cron/tick` alert + schedule evaluation. Falls back to `CRON_SECRET` when unset. Leave unset during pilot if you're not wiring a scheduler. |
| `APPROVAL_MAX_AGE_HOURS` | No                                            | Cutoff age (in hours) for `/api/v1/cron/approvals-expire`. Default `168` = 7 days.                                                                                 |

<Tip>
  When you wire an automated scheduler, match the cadence to the workload:

  * **tick**: every 1–5 minutes (matches the smallest `windowMinutes` across your alert rules)
  * **export-interactions**: Every hour or every 6 hours depending on data volume
  * **cleanup**: Once daily (e.g., `0 3 * * *`)
</Tip>
