Scheduled reports
Scheduled reports are recurring data exports that Distribu generates and emails on a fixed cadence. Four report types, three cadences, any number of recipients per report.
Manage them at
/dashboard/reports/scheduled.
The four report types
| Type | What it contains |
|---|---|
SALES | Daily revenue totals over the configured range: orders placed, gross revenue, average order value, broken down by status. |
CUSTOMER_SPEND | Per-customer spend summary: total orders, total revenue, last order date. One row per customer with activity in the range. |
INVENTORY_VALUATION | Current stock × current price per product — a snapshot of inventory value at the moment the report runs. |
ORDERS_EXPORT | The full orders table over the range. Two modes: summary (one row per order) or line-items (one row per line item). |
Output format
Each report is generated as either CSV or PDF based on the
format field. CSV is the default — pick PDF when you want something
you can hand to a colleague without further processing.
ORDERS_EXPORT also has a mode field that's only meaningful for
orders export:
summary— one row per order.line-items— one row per line item.
Cadence
| Cadence | Fires | Extra config |
|---|---|---|
DAILY | Once per day at the configured time | — |
WEEKLY | Once per week | dayOfWeek (1 = Mon … 7 = Sun) |
MONTHLY | Once per month | dayOfMonth (1–28) |
Monthly reports are capped at day 28 so the report always fires — no missed runs in February or on short months.
Recipients
A report has a list of recipient email addresses. Any number — usually one or two (you and the owner of a downstream system), but there's no enforced cap.
Recipients don't need to be Distribu users. Send reports to your accountant, your ERP's mailbox, your team Slack's email-bridge, whatever.
Creating a report
From /dashboard/reports/scheduled, click + New scheduled report.
The form collects:
| Field | Type | Notes |
|---|---|---|
reportType | enum | One of the four types above. |
format | csv / pdf | Default csv. |
mode | summary / line-items | Required for ORDERS_EXPORT; ignored otherwise. |
cadence | enum | DAILY, WEEKLY, MONTHLY. |
dayOfWeek | integer 1–7 | Required for WEEKLY. |
dayOfMonth | integer 1–28 | Required for MONTHLY. |
recipients | string[] | At least one valid email address. |
isActive | boolean | Defaults to true. |
On save, nextRunAt is computed forward from now based on the cadence.
Cron dispatch
Reports are fired by a daily cron at
GET /api/cron/scheduled-reports:
0 0 * * * # daily at 00:00 UTC
The cron loads every active report with nextRunAt <= now and
dispatches each one:
- Generate the report output (
buildReportOutput→ CSV bytes or PDF bytes, filename, content type). - Email the output as an attachment to the recipient list, with a branded Distribu email shell and a short summary in the body.
- On success: set
lastRunAt = now,lastRunStatus = "ok",failureCount = 0, and recomputenextRunAtforward. - On failure: set
lastRunStatus = "failed", store the error onlastRunError, incrementfailureCount, and fire aSCHEDULED_REPORT_FAILEDnotification to every OWNER / ADMIN on the company.
Audit entries are written in both cases: ScheduledReportSent on
success, ScheduledReportFailed on failure.
Anatomy of a scheduled report
| Field | Type | Notes |
|---|---|---|
id | string | Primary key. |
reportType | enum | SALES, CUSTOMER_SPEND, INVENTORY_VALUATION, ORDERS_EXPORT. |
cadence | enum | DAILY, WEEKLY, MONTHLY. |
format | string | "csv" or "pdf". |
mode | string, optional | "summary" or "line-items" — ORDERS_EXPORT only. |
dayOfWeek | int, optional | 1–7 for WEEKLY. |
dayOfMonth | int, optional | 1–28 for MONTHLY. |
recipients | string[] | Destination email addresses. |
isActive | boolean | Paused reports are skipped by the cron. |
nextRunAt | timestamp | When the next dispatch will fire. |
lastRunAt | timestamp, optional | When the last dispatch ran. |
lastRunStatus | string, optional | "ok" or "failed". |
lastRunError | string, optional | Error message on the last failure. |
failureCount | int | Consecutive failures. Resets to 0 on success. |
createdById | reference, optional | The staff member who created it. |
Failure handling
If a dispatch fails (email provider rejects, PDF renderer errors, etc.), Distribu doesn't immediately retry — the next attempt is on the next scheduled run. A persistent failure means:
- Each run increments
failureCountand re-firesSCHEDULED_REPORT_FAILEDto OWNER + ADMIN. - The report stays
isActive = trueuntil a human disables it. - The error message is visible on the report detail page — use it to debug (bad email address, Resend config, etc.).
There's no auto-disable after N failures today. If you need that, keep an eye on the notification stream.
Pausing or deleting
From the report detail page:
- Pause flips
isActivetofalse.nextRunAtis preserved; when you resume, scheduling continues from where it was (or the next valid future time). - Delete is permanent — the report row and its history go with it.
Manual runs
The scheduled-report system only fires on its cron schedule. For
one-off report generation, use the on-demand report pages under
/dashboard/reports (sales, customer spend, inventory valuation) —
these render immediately and offer a Download CSV / PDF button
without any scheduling.
What's not here (yet)
- Custom date ranges. Report windows are fixed relative to the run time (e.g., daily = "yesterday"; weekly = "last 7 days"). No "report the last 14 days every Monday" today.
- Custom column picking. Each report type's columns are fixed.
- Report templates or presets. You configure each report from scratch.
- Webhooks on report completion. Reports fire audit entries and (on failure) notifications, but don't currently post to a webhook.
Related:
- REST API — Reports endpoints — on-demand versions of the same reports, JSON over HTTP.
- Notifications — how
SCHEDULED_REPORT_FAILEDreaches you. - Audit log —
ScheduledReportSent/ScheduledReportFailedentries.
