Skip to main content
A Report schedule binds a template to a cron expression, timezone, and a list of notification-provider destinations.
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 — optional during pilot.
Schedules are evaluated once per tick by /api/cron/tick — the same endpoint that evaluates alert rules.

Schema

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. 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 UTC. 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 PATCH endpoint.
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.

destinations

Array of notification provider UUIDs. Each provider is looked up from the encrypted PlatformSetting 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:
{
  "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 ReportRun.
  • Run Now APIPOST /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:
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 for CRUD routes (POST /api/v1/reports/schedules, PATCH /api/v1/reports/schedules/:id, etc.).