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

# Report schedules

> Bind templates to cron cadences and notification destinations so reports run automatically — when the cron is wired. Without the cron, schedules still define nextRunAt but don't fire; use Run Now or /run-now API for on-demand delivery.

A **Report schedule** binds a template to a cron expression, timezone,
and a list of notification-provider destinations.

<Note>
  **Scheduled runs require the cron to be wired to hit `/api/cron/tick`.**
  During pilot / initial deployment this is not wired to AWS EventBridge by
  default, so schedules are defined but **will not fire automatically**.
  They'll still be created, persisted, and display `nextRunAt` — but that
  timestamp will not trigger anything until a scheduler invokes the tick.

  To deliver a report on demand during pilot, use the **Run Now** button in
  `/settings/reports` or the
  `POST /api/v1/reports/templates/[id]/run-now` API. Run Now works
  unconditionally and does not depend on the cron being wired.

  To enable automatic scheduled firing, see
  [EventBridge Setup](../self-host/deploy/eventbridge-setup) — optional during
  pilot.
</Note>

Schedules are evaluated once per tick by `/api/cron/tick` — the same
endpoint that evaluates alert rules.

## Schema

```prisma theme={null}
model ReportSchedule {
  id             String    @id @default(uuid())
  tenantId       String
  templateId     String
  name           String
  cronExpression String
  timezone       String    @default("UTC")
  destinations   Json      @default("[]")
  enabled        Boolean   @default(true)
  lastRunAt      DateTime?
  nextRunAt      DateTime?
  lastStatus     String    @default("pending")
  createdAt      DateTime  @default(now())
  updatedAt      DateTime  @updatedAt
}
```

## Cron semantics

Standard 5-field cron (`minute hour day-of-month month day-of-week`),
parsed by [`cron-parser`](https://github.com/harrisiirak/cron-parser).
Examples:

* `*/5 * * * *` — every five minutes
* `0 8 * * *`   — daily at 08:00
* `0 9 * * 1-5` — weekdays at 09:00
* `0 9 1 * *`   — first of every month at 09:00

Timezone defaults to coordinated universal time. Any IANA name
(`America/New_York`, `Europe/London`, …) is accepted.

## `nextRunAt` computation

On create and on every PATCH that modifies `cronExpression` or
`timezone`, the server recomputes `nextRunAt` using `cron-parser`.

On every cron tick that fires the schedule, `nextRunAt` advances to the
next occurrence **relative to the current tick time** — anchoring off
`now` prevents drift after restarts and missed ticks.

Invalid cron expressions park the schedule at `nextRunAt = null`. It
will not fire until an operator fixes the expression via the settings
UI or the schedule's PATCH endpoint.

<Warning>
  `nextRunAt` is informational only when the cron is not wired. The field
  shows when the schedule *would* fire if a scheduler were calling
  `/api/cron/tick` — setting or recomputing it does not itself trigger a
  run.
</Warning>

## `destinations`

Array of notification provider UUIDs. Each provider is looked up from
the encrypted **platform settings** vault; its `kind` determines the
payload shape:

* **ops\_email** — subject includes the template name; body = executive
  summary + key takeaways; artifacts attached as files.
* **slack / teams** — narrative + bullets + deep link to
  `/api/v1/reports/runs/[id]`. No attachments.
* **webhook** — POST body includes artifact base64 inline (under
  `extraPayload.artifacts[]`) so downstream consumers receive the
  content without a follow-up GET.

## `lastStatus`

Possible values after a cron tick:

* `completed` — run finished successfully; every destination delivered.
* `completed_with_errors` — run finished, but at least one destination
  delivery failed.
* `failed` — runner threw or returned a fatal error (typically a
  template-not-found condition).

When the cron hasn't been invoked yet, `lastStatus` stays at its default
(`pending`) and `lastRunAt` stays `null`.

## Tick behaviour

Per tenant, the tick:

1. `prisma.reportSchedule.findMany({ tenantId, enabled: true, OR: [{ nextRunAt: null }, { nextRunAt: { lte: now } }] })`
2. For each due schedule, invoke the runner with `triggeredBy: "cron"`,
   `dispatch: true`, `persistRun: true`.
3. Update `lastRunAt`, `nextRunAt`, `lastStatus` on the schedule.
4. A single schedule failing does not block the rest; an alert-evaluator
   failure does not block report processing.

The tick response JSON includes:

```json theme={null}
{
  "reportsEvaluated": 3,
  "reportsRan": 2,
  "reportErrors": [
    { "tenantId": "t1", "scheduleId": "sched-bad", "error": "Template tpl-missing not found" }
  ]
}
```

## Triggering a run without the cron

Two unconditional options work during pilot (or any time you want
immediate delivery):

* **Run Now button** — `/settings/reports` → pick a template → **Run Now**.
  This fires the runner immediately and creates a **report run** record.
* **Run Now API** — `POST /api/v1/reports/templates/[id]/run-now` with
  tenant context. Same runner path; returns the run ID.

Both bypass the scheduler entirely; neither requires `CRON_TOKEN` to be
set or EventBridge to be wired.

## Manual cron tick

For parity with scheduled behaviour (evaluate every due schedule in one
call), hit the cron directly:

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

This requires `CRON_TOKEN` to be set on the service. Any schedule whose
`nextRunAt <= now` fires during that tick. Useful for development and
for validating a schedule end-to-end before enabling EventBridge.

## API

See [Reports API reference](/api-reference/reports) for CRUD routes
(`POST /api/v1/reports/schedules`, `PATCH /api/v1/reports/schedules/:id`,
etc.).
