Scheduling Reports
Email reports on a recurring cadence and configure data-exception alerts
A scheduled report runs your saved report on a recurring cadence and emails the result as an attachment. An alert is a threshold rule that fires inside the same cron loop and emails recipients when a value crosses a line you set.
Both features live on the report viewer at /analytics/reports/[id], in the Schedules and Alerts tabs.
Scheduling a report
- Open a saved report you own (only the report creator can manage schedules).
- Click the Schedules tab.
- Click New schedule.
- Fill in:
- Name — a human label, e.g. "Monday morning enrollment digest"
- Cadence — Daily, Weekly, or Monthly
- Day of week / day of month — when the cadence requires it (Weekly picks a day 0–6, Monthly picks a day 1–28)
- Hour — UTC hour of day (0–23) when the schedule should fire
- Format — CSV, Excel (XLSX), or PDF
- Recipients — comma-, semicolon-, or newline-separated email addresses
- Click Create schedule.
The cron loop polls hourly. As soon as the schedule's nextRunAt reaches the current hour, the cron will:
- Run the report through the executor using your snapshotted role scope (so a teacher's schedule stays scoped to their own courses, an admin's to the whole institution)
- Render the result in the chosen format
- Email it to the recipients as an attachment
- Evaluate any alerts attached to the same report
- Recompute
nextRunAtfor the next firing
Cadence semantics
- Daily — fires every day at the chosen UTC hour
- Weekly — fires every week on the chosen day at the chosen UTC hour
- Monthly — fires every month on the chosen day-of-month at the chosen UTC hour. Day-of-month is capped at 28 to avoid Feb edge cases — pick "the 28th" for end-of-month delivery.
Timezones
In v1, the schedule stores a UTC hour directly. The dialog's hour picker is the UTC hour you want delivery to fire at. A local-time helper that auto-converts is on the v1.1 list.
If you want a 9am Asia/Kolkata delivery, that's hourUtc = 3. For 9am US/Pacific, that's hourUtc = 17.
Pausing and editing
- Toggle a schedule inactive by deleting it (in v1; an explicit pause toggle ships in v1.1).
- To change the cadence, delete the schedule and recreate it.
- Recipient list is not editable in v1 — same recipe.
Failures
If a schedule's run fails (e.g. the executor errors out), the row gets lastRunStatus = ERROR and lastRunError captures the message. The next hourly tick will retry — nextRunAt doesn't advance on failure, so you don't miss a delivery just because the database had a bad minute.
After 5 consecutive failures the v1.1 release will auto-pause the schedule and send the owner a notice. v1 keeps retrying indefinitely.
Alerts
Alerts let you say things like:
- "Email me when failed_count is greater than 5"
- "Email me when completion_rate is less than 0.6"
- "Email me when overdue_assignments is greater than or equal to 10"
Setting up an alert
- Click the Alerts tab on the viewer.
- Click New alert.
- Fill in:
- Name — what this alert is about
- Field — picked from the result. For aggregated reports this is the aggregation alias (e.g.
total_failed); for rows-mode reports it's a numeric column name. - Operator — greater than / less than / equal / etc.
- Threshold — a number
- Recipients — emails to notify when the alert fires
- Message (optional) — extra context included in the alert email
- Click Create alert.
How evaluation works
In v1, alerts are evaluated inside the schedule cron loop. After a scheduled run lands a result, the cron walks every active alert on the same report and checks whether any value matches the condition. If it does, the alert fires and sends an email to the alert's recipients (which can be different from the schedule's recipients).
This means an alert on a report with no schedule will never auto-fire. The Alerts tab shows a banner explaining this when no schedule exists. To enable alerts, add at least one schedule on the same report — even a daily one is enough for a check-in cadence.
A standalone alert cron that runs independently of schedules is on the v1.1 list.
Match semantics
- Buckets-mode results: the alert checks every bucket value in the matching aggregation series. The alert fires if any bucket value matches the condition.
- Rows-mode results: the alert checks the alert's
fieldvalue on every row. The alert fires if any row matches.
The email body includes the match count and a sample of up to 5 matched values.
Cooldown
There's no built-in cooldown in v1 — if your schedule fires daily and the condition is met every day, you'll get an alert email every day. Either:
- Tighten your threshold so the condition only matches when you really care
- Pause the alert by deleting it temporarily
- Wait for v1.1 which adds an
lastTriggeredcooldown window (default 24h)
Audit trail
Every schedule run writes a report.schedule.run row to LMSAuditLog, and every alert fire writes a report.alert.fire row. Both include the report id, the recipients touched, and the row count.
The LMSReportRun table also tracks one row per scheduled execution with triggeredBy = "schedule", separate from manual runs (triggeredBy = "manual").
Tips
- Schedule the report your team already screenshots and pastes into Slack on Monday. That's the highest-value use case.
- Pick PDF for status reports, XLSX for analyst hand-offs, CSV for programmatic pipelines. The format is the schedule's "delivery shape" — pick the one that matches who reads it.
- Set the alert threshold one notch tighter than your "actual concern" line. That way you get a heads-up before things become urgent, not the moment they're already on fire.
- Don't over-recipient. Each recipient gets a copy of the file as an attachment. Three recipients × 4MB PDF = 12MB of email. For wide distribution, consider sending to a single mailing list.