Reports
Available
- Report schedules on flows (
settings.reports), time and event modes - Email delivery of a 7-day link
- Send now (
POST /v1/flows/{id}/reports/send-now) - Per-flow history
- Public payload endpoint (
GET /reports/{token}) - GUI report page (
/report/{token}) with PDF download commsadapter deliveryreport.generatedwebhook events
A report is a frozen snapshot of one run’s per-target results (trigger state, colors,
probability, and every tile’s values) stored by the Engine and shared by link. The Engine
owns scheduling, generation, delivery and history; rendering is the GUI’s job: the
emailed link opens the public report page (/report/{token}), which fetches the payload
and draws it (HTML plus a Download PDF of the same frozen snapshot). Headless integrations can
consume the payload JSON directly.
Configuration: settings.reports
Report config rides the flow definition as a validated block:
{
"reports": {
"schedules": [{
"id": "morning", // stable per-schedule id (your choice, ≤64 chars)
"name": "Morning digest",
"enabled": true,
"mode": "time", // "time" (calendar) or "event" (on trigger)
"months": [], // 1–12; empty = every month
"weeksOfMonth": [], // 1–4, 5 = "last week"; empty = every week
"daysOfWeek": [1, 3, 5], // 0–6, Sunday = 0; empty = every day
"timeOfDay": "08:00", // matched at hour granularity; default 08:00
"timeZone": "America/Denver", // IANA; default UTC
"targetFilter": "all", // all | triggered | not_triggered
"contactListIds": ["…"]
}],
"brand": { "name": "Acme Vineyards", "logo": "https://…" } // optional flow brand
}
}The optional brand overrides the tenant brand per field for this flow’s
reports and their emails; unset fields fall back to the tenant brand.
Modes
- Time: an hourly Engine pass finds due schedules (calendar match in the schedule’s time zone), forces a fresh run of the flow, and sends the report when the run finishes. One send per hourly slot per schedule.
- Event: fires at the end of a scheduled run in which any target triggered, throttled to one send per schedule per 6 hours. Manual runs never fire event reports.
- Send now:
POST /v1/flows/{id}/reports/send-nowfreezes a report from an existing succeeded run (the latest by default) and emails it to the given contact lists; the response carries the link immediately. No throttle.
Delivery
Email only. Recipients resolve from the schedule’s contact lists (de-duplicated); a contact
is emailed only when they have an address, emailOptIn, and a verified address;
everything else is recorded as skipped_optout / skipped_unverified in the history. The
email subject is "{brand} Report: {flow}".
By default the Engine sends through the platform email service. A tenant can
register a comms adapter, a sandboxed script (ctx, req) => ({ok, error?}) receiving
{channel, to, subject?, message, html?}, to send through their own provider instead; it
applies to alert delivery too. See Adapters.
The link and the payload
Every report gets an opaque token (grt_…, stored hashed) valid for 7 days. The emailed
link points at the GUI’s report page; the data behind it is public JSON:
GET /reports/{token}
→ { flowName, generatedAt, reportDate, notes, dataWindow, temperatureUnits, colors,
brand: {name, logo}, flowTiles, targets: [{name, boundary?, triggered, probability,
color, runAt, tiles: [...points...]}], expiresAt }Unknown or expired tokens 404. Anyone with the link can read the payload until it expires, so treat report links like the shared documents they are.
History
GET /v1/flows/{id}/reports lists send attempts newest-first: schedule, mode
(time | event | manual), status (sent | skipped | error), reason, target counts, and
per-recipient outcomes. Event-mode sends suppressed by the 6-hour throttle are silent (no
history row).