Skip to main content

Scheduled reports: cadence, recipients, and the send-now path

A scheduled report is an analytics rollup Orbit emails to a fixed recipient list on a recurring cadence. Each report has a name, a report type (the metrics section it contains), a frequency (daily, weekly, or monthly), a recipients list of 1–50 email addresses, and an optional IANA timezone the cadence anchors to. The report arrives as an email with the metrics rendered in the body and a PDF attached. Report types: All scheduled-report endpoints live under /api/v1/analytics/scheduled-reports. This is the only write surface for scheduled reports — the older /api/v1/reports/scheduled CRUD was removed; any report it created was migrated automatically, so nothing was lost.

Create a report

Create one with POST /api/v1/analytics/scheduled-reports:
  • recipients accepts an array of emails or one comma-separated string; duplicates (case-insensitive) are dropped. Every entry must be a valid email, and the list is capped at 50 addresses. Expect a 400 (VALIDATION_ERROR) when a recipient is malformed.
  • timezone is optional. When set, the cadence anchor is validated as an IANA zone name (America/New_York, Asia/Tehran, …). When omitted, the cadence anchors to UTC.
  • enabled defaults to true; create with "enabled": false to stage a report without sending it.
  • filters is an optional JSON object. For custom reports it carries the saved-query definition, and filters.dataset must be messages or queue_performance when present.

How the cadence tick is computed

Every report fires at 09:00 local time — never at the moment you created it, and never a raw interval like NOW() + 30 days:
  • daily → next day at 09:00
  • weekly → the start of next week at 09:00
  • monthly → the first day of the next month at 09:00
Anchoring to calendar boundaries matters: an interval-based monthly schedule drifts by several days per year, and a monthly report created on the 31st would slowly migrate earlier in the month. Anchoring keeps a monthly report on the 1st and a weekly report on the same weekday, indefinitely. When the stored next_send_at instant has already arrived — for example right after a send-now request, or in the short window between a tick becoming due and the scheduler claiming it — the API does not echo that past/now value back. Because send-now works by marking the report due, the raw value would otherwise read as “now” (or a past instant) even though the recurring schedule didn’t change. Instead the API returns the upcoming cadence tick, so the value you read (and the dashboard renders) is always the next real 09:00 tick, never a transient due-time.

Edit cadence, recipients, or sections

PATCH /api/v1/analytics/scheduled-reports/{id} accepts any subset of fields; omitted fields keep their current values. The endpoint requires at least one field.
Two update behaviors are worth knowing:
  • Frequency changes re-anchor the schedule. When you change frequency, the next tick is recomputed from the last send (or from now if the report never sent), floored so the result can never be in the past. So switching a stale weekly report to daily makes the next tick tomorrow at 09:00 — not next week minus the elapsed days. It also never triggers an immediate send as a side effect.
  • Recipient edits replace the list. recipients is replaced wholesale, not merged — pass the full final list on every update. The same 1–50 unique-valid-email rules apply.

Send-now: what it does and doesn’t do

send-now marks the report due; the scheduler worker picks it up on its next tick and delivers it. It does not build and send the email inside your HTTP request — the response returns immediately with { "queued": true }, keeping send/retry logic out of the API tier. Poll GET /api/v1/analytics/scheduled-reports and watch last_sent_at to see when it actually lands. It also doesn’t move the cadence. Because the list endpoint presents the upcoming 09:00 tick whenever a report is due, a daily report keeps showing “tomorrow 09:00” after send-now rather than collapsing to “a few minutes from now.”

Delete

Deleting is permanent and returns { "id": "sr_01H…", "deleted": true }. Deleting stops all future sends; previously sent emails are of course unaffected. To pause a report without losing its recipient list and schedule, set "enabled": false on a PATCH instead of deleting it.

Role gate

Reads (GET) are open to any authenticated member of the organization. Writes — create, update, delete, and send-now — require the owner or admin role. A scheduled report emails recipient-level metrics (top contacts, spend, error breakdowns) to an arbitrary list of addresses the creator chooses: that is a data-egress path, so it’s restricted to roles trusted to export org data. If viewer-role tokens could create reports, a read-only member could exfiltrate the same analytics to any mailbox. The role check comes back as a 403 on write endpoints for developer/viewer keys and members.

Troubleshooting

See also