Use when creating, previewing, updating, scheduling, or exploring Costory reports. Route by intent: scheduled Slack/Teams/email sharing (Schedule — ask before build) vs explain last-month cost with preview-first DIGEST (Explain — chat preview required; not query+compare). Workflows are named, never lettered. DIGEST AI is opt-in via display: \"summary\" and/or enableAiInvestigation — both slower. Covers GRAPH_SNAPSHOT, TOP_FLOP, DASHBOARD_PDF, context-first inheritance, destinations after chan...
Installs into .claude/skills of the current project.
Are you the author of Reports?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/costory-io-reports)
---
name: reports
description: "Use when creating, previewing, updating, scheduling, or exploring Costory reports. Route by intent: scheduled Slack/Teams/email sharing (Schedule — ask before build) vs explain last-month cost with preview-first DIGEST (Explain — chat preview required; not query+compare). Workflows are named, never lettered. DIGEST AI is opt-in via display: \"summary\" and/or enableAiInvestigation — both slower. Covers GRAPH_SNAPSHOT, TOP_FLOP, DASHBOARD_PDF, context-first inheritance, destinations after channel type is known, and delivery safety. Call get_skill with skillId \"reports\" before create_report or substantial report work."
---
# Reports
**Skill body version 0.5.3.** Workflows here are **named** — Schedule, Explain, Update, Run, Explore. Older bodies lettered them A–E, and other Costory surfaces used a different letter order. If you are holding a lettered routing table for reports, it is stale: route by the names in this body and ignore the letters.
A **report** has a shared **`reportContext`** input (global theme) and **widgets** that inherit it by default. It delivers those widgets (chart snapshot, PDF, top/flop, text, or **DIGEST** cost-change tree) to one or more destinations (Slack, Teams, email). Same mental model as dashboards: shared context + per-widget overrides. `create_report`, `update_report`, and `preview_report_widget` all take the same report-level `reportContext`.
> **Temporary compatibility:** the deprecated top-level `context` alias is accepted during migration, but always generate `reportContext`. Never send both.
**Load this skill first** for any report creation, DIGEST preview, substantial update, or delivery conversation.
## Must-follow rules
Everything below this section is detail. These eight are the contract:
1. **Ask before build (Schedule / delivery).** Do not pick widgets, call `preview_report_widget`, or call `create_report` until the design is confirmed. Never silently default to a graph or an AI summary on Schedule.
2. **Explain exception — preview-first DIGEST.** For one-shot *"what changed / explain last month"* (Explain workflow or `explain-period-change` recipe), **defaulting to DIGEST chat preview is required.** Do not wait for a full design questionnaire; do not substitute `query` + `compare` for the first answer.
3. **Context-first.** Once answers are in, define the full `reportContext` before listing widgets. Widgets carry **only overrides** — never duplicate what already lives in `reportContext`.
4. **Destinations last.** Do not call `list_available_destinations` until the user has named the channel **type** (Slack / Teams / email). Then list only that type and propose matches by name — never paste a whole channel list into the conversation.
5. **AI features are opt-in.** DIGEST-only. Set `display: "summary"` for the executive narrative and/or `enableAiInvestigation: true` for per-node deep analysis — both default off (tree-only) and are slower.
6. **Confirm delivery.** Both `NOW` and `SCHEDULED` need explicit confirmation before `create_report`. `UNSCHEDULED` does not.
7. **`datePreset`, never frozen dates,** on `SCHEDULED` reports.
8. **Cost data is ~2 days late.** Prefer a **Tuesday** weekly send (`weekday: 2`) when the period is `LAST_WEEK` (a completed calendar week). If the report must land earlier (e.g. Monday), use `TRAILING_7_DAYS` instead of `LAST_WEEK` so the window is not a half-empty calendar week.
## Routing
| User intent (examples) | Workflow |
|------------------------|----------|
| "Generate a scheduled report / message for Slack, Teams, or email" | **Schedule** |
| "I want to share FinOps information with my team" (recurring or channel delivery) | **Schedule** |
| "I need to explain last month's cost" / "what changed last month and why" | **Explain** (NOW + DIGEST) |
| Update an existing report | **Update** |
| Run / retry / transfer / archive | **Run** |
| Explore a delivered DIGEST execution | **Explore** |
If intent is ambiguous between Schedule and Explain, ask: **recurring delivery to a channel**, or **one-shot explanation of last month's costs?**
## Tool order
1. `get_skill` with `skillId: "reports"` — this guide
2. `get_context` — org context, currency, popular groupBys
- **Several organizations:** if `get_context` returns "Multiple organizations" with a list of slugs, do not pick one. Ask the user which org to use (from that list or `list_organizations`), then pass that `slug` to `get_context` and the following tool calls. If the user already named the org (e.g. "org `acme`"), use that slug directly.
3. `list_teams` — only when team scoping may apply; each scope's `id` is the `scopeId` value
4. `suggest_groupby` — when a DIGEST hierarchy is open-ended; needs the planned period + the report's scope filter
5. `list_available_destinations` — **only after the channel type is known**, and only for a delivered report
6. `preview_report_widget` → `create_report` / `update_report` — only after the design brief is confirmed
Destination discovery is a late step, not a warm-up. An org can have dozens of Slack channels; enumerating them before the user has said "Slack" (or "Teams", or "email") buries the design conversation in noise. Once the type is known, list only that type — and if the filtered list is still long, ask for a channel name or keyword and match against it rather than echoing every entry.
---
## Workflow: Schedule — create a scheduled report
**Triggers:** scheduled message for Slack / Teams / email; share FinOps information with a team on a cadence.
**Goal:** accompany the user to **design** a recurring report they will actually use, then create a `SCHEDULED` report. Do not assume DIGEST or jump to create.
**Tone:** collaborative design partner. Explain options in plain language, offer concrete starters, confirm a short design brief before any preview or create. The user should feel guided, not interrogated with a long form.
### 1 — Open the design conversation
Acknowledge the goal (recurring FinOps delivery), then ask a few open questions in one short turn:
1. **What should this deliver?** — what should recipients learn or do when it lands?
2. **Who / where?** — Slack, Teams, or email (the type only; concrete destinations come at build time)
3. **How often?** — weekly or monthly (rough is fine)
If they are unsure what to put in the report, do not wait passively — go straight to step 2 and offer starter ideas.
### 2 — Offer starter reports (proactively)
Present 3–5 options as a friendly menu (value prop first, then widgets). Invite pick / mix / customize:
| Starter | Plain-language pitch | Widgets | Typical schedule / presets |
|---------|----------------------|---------|----------------------------|
| Monthly cost digest | "What changed last month?" — ranked movers as a tree; optional AI narrative for execs | DIGEST (± optional AI summary) | MONTHLY · `LAST_MONTH` |
| Weekly team pulse | "What blew up or dropped for each team last week?" | TOP_FLOP ± GRAPH_SNAPSHOT | WEEKLY · Tuesday + `LAST_WEEK` (or Monday + `TRAILING_7_DAYS`) / `TRAILING_14_WEEKS` |
| Migration / spend tracker | "Are we on track?" — trend over weeks + last-week movers | GRAPH_SNAPSHOT + TOP_FLOP | WEEKLY · Tuesday · GRAPH `TRAILING_14_WEEKS`, TOP_FLOP `LAST_WEEK` (or `TRAILING_7_DAYS` if not Tuesday) |
| Dashboard pack | "Send our existing FinOps dashboard on a cadence" | DASHBOARD_PDF | WEEKLY + weekday · period lives on the dashboard |
| Invoice / finance close | "Close the books on what moved last invoice month" | DIGEST (± DASHBOARD_PDF; optional AI summary) | MONTHLY · `LAST_INVOICE_MONTH` or `LAST_MONTH` |
Example framing: *"Here are a few patterns that work well for teams. Which fits, or should we combine two?"*
For a **concrete, business-specific design** (marketplace spend, namespace cost, provider credits, untagged coverage, env costs, …) load the **`recipes`** skill and Read the matching card — it comes back as a confirmed brief you build with this workflow.
### 3 — Deepen the design (after they pick a direction)
Fill only what is still missing — keep it conversational:
1. **Scope** — whole org, `scopeId` from `list_teams`, and/or `conditionsCel`
2. **Split / hierarchy** — if DIGEST is in the mix: confirm root + deeper levels, or run `suggest_groupby` and propose a tree in plain language
3. **Optional AI** (DIGEST only) — ask explicitly: executive narrative (`display: "summary"`) and/or per-node investigation (`enableAiInvestigation: true`); both slower; default tree-only (`display: "tree"`, investigation off)
4. **Cadence details** — WEEKLY needs `weekday` (0 = Sunday … 6 = Saturday); set `firstRunAt` (ISO-8601 UTC); use presets that roll forward (`LAST_WEEK`, `TRAILING_7_DAYS`, `LAST_MONTH`, …). Refuse frozen `from`/`to` on SCHEDULED reports. **Data lag:** billing/cost data is typically ~2 days behind. For a calendar-week pulse, default `weekday: 2` (Tuesday) with `LAST_WEEK`. If they insist on Monday (or another early weekday), switch the period to `TRAILING_7_DAYS` — do not pair Monday + `LAST_WEEK` (that full week still has incomplete late days).
5. **Currency / metric** — usually `cost` + org currency from `get_context`
The channel **type** is already known from step 1; the concrete destination is resolved at build time, not here.
Then restate a **one-paragraph design brief** (audience, cadence, widgets, scope, AI narrative / investigation yes/no) and get confirmation before building.
### 4 — Build
1. `get_context`, plus `list_teams` only if team scoping applies
2. Steps 1–3 above → design brief confirmed
3. If the DIGEST hierarchy was open-ended → `suggest_groupby` with the planned period + scope filter → propose root + `additionalGroupBy` → confirm
4. **Draft `reportContext` first** — shared `datePreset` / metric / currency / `groupBy` / scope
5. If DIGEST is in the mix → `preview_report_widget` (defaults **100 / 5% / 20**; if they named a row count, set `topLargestAbsoluteChange` to that integer **1–100**; set `display: "summary"` / `enableAiInvestigation` only if opted in) → tune → re-preview
6. **Now** resolve delivery: `list_available_destinations` for the chosen channel type → propose matches by name → confirm the specific destination. Missing Slack/Teams integration → https://app.costory.io/integration
7. Confirm `schedule.mode: "SCHEDULED"` (period, weekday, `firstRunAt`) → `create_report` with the **same `reportContext` + widgets** you previewed
---
## Workflow: Explain — last month (NOW + DIGEST)
**Triggers:** explain last month's cost; what changed last month and why; one-shot narrative for stakeholders. Prefer recipe `explain-period-change` when that card was loaded.
**Goal:** a **chat DIGEST preview** (then optional NOW) whose primary widget is **DIGEST**, with hierarchy from `suggest_groupby`. The core deliverable is the **change tree from `preview_report_widget`**; do not rebuild it with `query`.
### 1 — Defaults vs questions
**Chat-only (default when they did not name a channel):** assume whole org, `LAST_MONTH` (or `LAST_INVOICE_MONTH` if they said invoice), tree-only AI off. Ask only if scope/period is ambiguous.
**Before NOW delivery** (they asked to send): confirm scope, hierarchy, AI (`display: "summary"` / `enableAiInvestigation`), and channel type. GRAPH_SNAPSHOT only if they also want a trend image.
Do not skip to a graph-only report for this trigger — the core ask is explanation → DIGEST tree. Do not call `query` for the first answer.
### 2 — Discover the tree with `suggest_groupby` (same turn as preview)
1. `get_context` (+ `list_teams` only if team scoping is needed)
2. Resolve period: prefer `reportContext.datePreset: "LAST_MONTH"` (or `LAST_INVOICE_MONTH`)
3. Call `suggest_groupby` with `from`/`to` matching that month and the same `filterCel` as the planned scope
4. Pick a tree: **root** = top hit → `reportContext.groupBy`; **deeper levels** = next useful hits → `additionalGroupBy` (typically 1–2 levels). If suggestions are empty/weak → `popularGroupBys`, else `cos_provider` → `cos_service_name`
5. **Chat-only:** state the path in one sentence and go straight to preview (no confirmation wait). **NOW:** confirm the path before create
### 3 — Preview DIGEST (primary data tool)
1. Draft `reportContext`: `datePreset: "LAST_MONTH"`, chosen `groupBy`, `metricId`, `currency`, optional scope
2. `preview_report_widget` with `{ reportContext, widget }` (**singular** `widget`, never `widgets`) — minimal DIGEST (`additionalGroupBy`, thresholds **100 / 5% / 20** unless they named another `topLargestAbsoluteChange` in **1–100**, `aggBy: "Month"`; `display: "summary"` / `enableAiInvestigation: true` only if opted in)
3. **Present only preview fields** — required shape:
- Headline from `resolvedPeriod` + `totals`
- `topIncreases` / `topDecreases` (path + Δ)
- Tree outline from `rootNodes` (label, Δ, childCount)
- Footer: thresholds + optional `explorerUrl` / `comparisonPeriodSummary`
- `summaryMarkdown` only when `display: "summary"`
4. Stop. Offer drill-down / AI re-preview / NOW. Tune from `recommendations` → re-preview — still without `query` unless the user names a node to dig into
### 4 — Deliver
1. Stop here if they only wanted an explanation in chat — no `create_report` needed
2. To send: channel type known → `list_available_destinations` for that type → confirm the channel → confirm `schedule.mode: "NOW"`
3. `create_report` with the **same** `reportContext` + that DIGEST object inside **`widgets: […]`** (plural array; GRAPH_SNAPSHOT only if requested)
---
## Workflow: Update
`get` (report id) → preview if DIGEST content changes → `update_report`.
- Patch `reportContext` alone to change the report-wide period / groupBy / filter / currency without rewriting widgets.
- Pass `widgets` only when replacing the widget list (wholesale replace). The field is always the `widgets` **array** — a singular `widget` key is silently ignored.
- New widgets follow the same inheritance rules: omit fields that match `reportContext`.
## Workflow: Run — run, retry, transfer, archive
Retry **failed executions only** — never `run_report_now` to fix one destination. Do not poll executions in an unbounded loop.
## Workflow: Explore — delivered DIGEST
`get_report_execution` → `get_report_execution_widget` with `view: "tree"` for the full formatted tree.
---
# Payload cookbook
Read this half when drafting JSON — the conversation rules above still apply.
## Report `reportContext` fields
| Field | Role | Widget inheritance |
|-------|------|-------------------|
| `metricId` | Default cost column (e.g. `"cost"`) | Cost widgets omit `metricId` to inherit |
| `currency` | USD, EUR, GBP, CNY | Cost widgets omit `currency` to inherit |
| `groupBy` | Default split — for DIGEST this is the **root** hierarchy axis | Cost widgets omit `groupBy` to inherit; DIGEST deeper levels go in `additionalGroupBy` |
| `datePreset` **or** `startDate`/`endDate` | Report period — prefer `datePreset` for scheduled reports | Widgets omit `from`/`to`/`datePreset` to inherit |
| `conditionsCel` | Report-wide CEL filter (scope) | Merged into every cost widget at runtime |
| `scopeId` | Optional saved team scope | Report-wide unless a widget overrides |
Each query still needs `"type": "cost"` — the query kind is not inherited.
Report **ownership** (`teamId`, `visibility` on `create_report`) is separate from `scopeId` / `conditionsCel` (query filter only). Ownership controls who sees and edits the report; scope controls which costs the widgets query.
## Inheritance resolution
At preview, create, and each scheduled run:
1. Start from `reportContext` (period, `metricId`, `currency`, `groupBy`, `conditionsCel`, `scopeId`).
2. Apply **widget-level** overrides (`datePreset` / `from`/`to`, `scopeId`, DIGEST thresholds, `aggBy`, title, …).
3. Apply **query-level** overrides (`groupBy`, `metricId`, `currency`, `filterCel`, `chartType`, `alias`).
4. Effective cost filter = report `conditionsCel` **AND** widget/query `filterCel` (an empty side is a no-op).
Consequences worth knowing:
- **Similarity rule:** when widgets share a period, split, metric, currency, or scope, put it in `reportContext` first. Widget fields are exceptions only.
- **Per-widget periods are normal** — e.g. GRAPH = `TRAILING_14_WEEKS`, TOP_FLOP = `LAST_WEEK` (Tuesday send) or `TRAILING_7_DAYS` (earlier weekday). Keep shared fields on `reportContext`; override only `datePreset` (and presentation) on the widget.
- **Never combine** `datePreset` with explicit dates on the same layer.
- **`conditionsCel` is report-wide** — do not repeat it inside every widget `filterCel`.
- **TEXT and DASHBOARD_PDF do not inherit query fields.** PDF period lives on the referenced dashboard; TEXT is static markdown in `contentMarkdown` (not dashboard `textContent`).
- Use `startDate`/`endDate` (or widget `from`/`to`) only for truly custom one-off ranges — never on `SCHEDULED` reports.
## Widget types
| Type | What it delivers | Best for |
|------|------------------|----------|
| `DIGEST` | Cost-change **tree** vs previous period; optional `display: "summary"` narrative and/or `enableAiInvestigation` | Explain what moved (monthly / weekly) |
| `GRAPH_SNAPSHOT` | Chart image from a cost/usage/metric query | Trends, migration tracking, composition over time |
| `TOP_FLOP` | Ranked increases / decreases for a period | "What blew up / what dropped last week?" |
| `DASHBOARD_PDF` | PDF of an existing dashboard (`dashboardId`) | Weekly/monthly pack of a known dashboard |
| `TEXT` | Static markdown via `contentMarkdown` | Context, how-to-read notes, links (**not** live AI) |
A report can combine several widgets. Prefer variety of **questions**, not duplicate charts.
## DIGEST AI options (`display` + `enableAiInvestigation`)
Two independent DIGEST fields control AI (both opt-in; both slower):
| Field | Values / default | What it does |
|-------|------------------|--------------|
| `display` | `"tree"` (default) \| `"table"` \| `"summary"` | Presentation. `"summary"` = LLM executive narrative (`summaryMarkdown`). `"tree"` / `"table"` = structured view only (faster). |
| `enableAiInvestigation` | `false` (default) \| `true` | Per-node deep analysis on deepest-leaf movers (async; delivery waits). **Independent** of `display`. |
Analysis presets (map intent → fields; do not invent other knobs):
| Intent | Fields |
|--------|--------|
| Direct tree (default) | `display: "tree"`, `enableAiInvestigation: false` (or omit both) |
| Executive summary | `display: "summary"`, `enableAiInvestigation: false` |
| Deep investigation | `display: "summary"`, `enableAiInvestigation: true`, `topLargestAbsoluteChange: 20` (any integer **1–100**; a higher N multiplies per-level work) |
- **Offer narrative** when they want a written overview of what changed and why (exec / stakeholder read). Set `display: "summary"`.
- **Offer investigation** only when they want per-node AI explanations under the tree — warn it is noticeably slower. Set `enableAiInvestigation: true`. Prefer `display: "summary"` with it so the narrative can ground on those findings; do not ask a second tree-vs-summary question once deep investigation is chosen.
- **Say the trade-off out loud** before enabling either. Ask plainly: *"Tree-only (faster), an AI executive summary, or deep per-node investigation (slowest)?"*
- **Default to tree-only** unless they opt in. Never invent a separate AI widget or fake a summary via TEXT.
- Only DIGEST produces `summaryMarkdown` — never claim one from GRAPH_SNAPSHOT, TOP_FLOP, TEXT, or DASHBOARD_PDF.
Pairings: tree is enough → DIGEST alone (defaults). Narrative + tree → DIGEST with `display: "summary"`. Deep dig → DIGEST with `display: "summary"` + `enableAiInvestigation: true`. Trend or movers only → GRAPH_SNAPSHOT / TOP_FLOP. Static intro → TEXT.
## TEXT widgets
Free-form markdown block. Shape is **`{ type: "TEXT", contentMarkdown }`** — optional `title` / `description`.
- Field name is **`contentMarkdown`** (1–10,000 characters after trim). Do **not** use `textContent` — that is the **dashboards** text-widget field (`type: "text"`).
- No queries, period, or inheritance from `reportContext`.
- Not a substitute for the DIGEST AI summary (`summaryMarkdown`).
```json
{
"type": "TEXT",
"title": "How to read this report",
"contentMarkdown": "## Weekly pulse\n\nFocus on the TOP_FLOP movers below, then open the graph for trend context."
}
```
## DIGEST hierarchy
DIGEST builds a tree. Levels map to fields:
| Level | Field | Notes |
|-------|-------|-------|
| Root (top of tree) | `reportContext.groupBy` | Preferred; use `queries[0].groupBy` only when one DIGEST must differ |
| Deeper levels (in order) | `additionalGroupBy` | Ordered children only — **never** put the root here |
| User says | `reportContext.groupBy` (root) | `additionalGroupBy` |
|-----------|---------------------------|---------------------|
| provider → service | `cos_provider` | `["cos_service_name"]` |
| env → project → service | `environment` | `["cos_sub_account_id", "cos_service_name"]` |
| team → service | `team` | `["cos_service_name"]` |
- Confirm the resolved path with the user when they describe a hierarchy in plain language (e.g. "production → Costory → AmazonEC2").
- Discover CEL field names via `search` with `type: ["dimensions"]`, or from `get_context` popularGroupBys.
- For an open-ended hierarchy ("good split for last month"), call `suggest_groupby` with the planned period + the same `filterCel` / scope as the report — top hit as root, next hits as `additionalGroupBy` candidates. Prefer dimensions that also appear in popularGroupBys when ties exist.
- Never invent a deep tree without confirming it.
## Query config (DIGEST, GRAPH_SNAPSHOT, TOP_FLOP)
Same query shape as `query`, minus anything that matches `reportContext`:
- `queries` — for DIGEST: exactly one `{ type: "cost", name: "a", filterCel? }` (omit `groupBy` / `metricId` / `currency` when they match reportContext)
- **Period** — omit on widgets when `reportContext.datePreset` (or start/end) is set; widget-level `datePreset` / `from`/`to` are overrides only. Never hand-compute dates for recurring schedules. For DIGEST the server also resolves the comparison period and echoes `resolvedPeriod`.
- `aggBy`
- Widget `scopeId?` only when it must differ from `reportContext.scopeId`
## How many rows
KPI movers (`TOP_FLOP`) and the DIGEST tree both take an integer N. Pass the integer they asked for (**1–100**).
| Widget | Fields | Range | Default | Map their words |
|--------|--------|-------|---------|-----------------|
| `TOP_FLOP` | `topN` and `flopN` | integer **1–100** to choose a size; persist also accepts **0** | **5** each | "top 3" → `3`; "biggest change only" → `1`; a custom count (`7`, `40`, `100`) → that integer. When they name one count, write that N to both fields. `flopN: 0` (or `topN: 0`) hides that side — increases only, or decreases only. Values above **100** are rejected. |
| `DIGEST` | `topLargestAbsoluteChange` | integer **1–100** | **20** | Same phrasing. Applied at **every tree level**, so a high N multiplies retained nodes — and AI investigations / summary tokens when those are on. `0` and values above **100** are rejected. |
## Preview defaults (DIGEST)
| Field | Default |
|-------|---------|
| `minAbsoluteDiff` | **100** |
| `minRelativeDiff` | **5** (percent) |
| `topLargestAbsoluteChange` | **20** (any integer **1–100**) |
| `display` | **`"tree"`** (set `"summary"` only when opted in) |
| `enableAiInvestigation` | **`false`** (set `true` only when opted in) |
Always show `resolvedPeriod` (when present), `comparisonPeriodSummary`, `totals`, `counts`, `topIncreases` / `topDecreases`, and `rootNodes`. Show `summaryMarkdown` only when `display: "summary"` (and warn that that preview may take longer).
Tune from `recommendations`: thresholds, `topLargestAbsoluteChange` (any integer 1–100; lower it when the tree is large — it applies at every level), grouping. Repeat `preview_report_widget` until satisfied.
## Examples
**`preview_report_widget` — monthly DIGEST (tree-only defaults):**
```json
{
"reportContext": {
"datePreset": "LAST_MONTH",
"groupBy": "environment",
"metricId": "cost",
"currency": "USD"
},
"widget": {
"type": "DIGEST",
"queries": [{ "type": "cost", "name": "a", "alias": "Cost by environment" }],
"aggBy": "Month",
"additionalGroupBy": ["cos_sub_account_id", "cos_service_name"],
"minAbsoluteDiff": 100,
"minRelativeDiff": 5,
"topLargestAbsoluteChange": 20
}
}
```
**Same DIGEST widget with executive AI summary** (add on the widget when opted in):
```json
{
"type": "DIGEST",
"queries": [{ "type": "cost", "name": "a", "alias": "Cost by environment" }],
"aggBy": "Month",
"additionalGroupBy": ["cos_sub_account_id", "cos_service_name"],
"minAbsoluteDiff": 100,
"minRelativeDiff": 5,
"topLargestAbsoluteChange": 20,
"display": "summary",
"enableAiInvestigation": false
}
```
**`create_report` / `update_report` — same DIGEST goes in `widgets` (plural array).**
**Migration tracker (graph + top/flop — not DIGEST).** Pair with `schedule.weekday: 2` (Tuesday) when using `LAST_WEEK`; if the send day is Monday, set `reportContext.datePreset` to `TRAILING_7_DAYS` instead:
```json
{
"reportContext": {
"metricId": "cost",
"currency": "USD",
"groupBy": "team",
"datePreset": "LAST_WEEK"
},
"widgets": [
{
"type": "GRAPH_SNAPSHOT",
"title": "Cost by team — trailing weeks",
"queries": [{
"type": "cost",
"name": "a",
"alias": "Cost by team",
"chartType": "LINE"
}],
"datePreset": "TRAILING_14_WEEKS",
"aggBy": "Week"
},
{
"type": "TOP_FLOP",
"title": "Last week movers by team",
"queries": [{
"type": "cost",
"name": "a",
"alias": "Cost by team"
}],
"aggBy": "Period",
"topN": 5,
"flopN": 5
}
]
}
```
**Weekly dashboard PDF widget:**
```json
{
"type": "DASHBOARD_PDF",
"dashboardId": "<id from search>"
}
```
**TEXT intro (report field is `contentMarkdown`, not dashboard `textContent`):**
```json
{
"type": "TEXT",
"title": "How to read this report",
"contentMarkdown": "## Weekly pulse\n\nFocus on the TOP_FLOP movers below."
}
```
**Full `create_report` (scheduled):**
```json
{
"visibility": "PRIVATE",
"schedule": {
"mode": "SCHEDULED",
"period": "MONTHLY",
"firstRunAt": "2026-08-01T10:00:00.000Z"
},
"reportContext": {
"datePreset": "LAST_MONTH",
"groupBy": "environment",
"metricId": "cost",
"currency": "USD"
},
"widgets": [{ "type": "DIGEST", "...": "minimal widget overrides only" }],
"destinations": [{ "destinationType": "SLACK", "channelId": "C…" }]
}
```
## Payload anti-patterns
The conversation rules live in **Must-follow rules** above. These are the shape mistakes:
- Do not put the root hierarchy axis in `additionalGroupBy` instead of `reportContext.groupBy`
- Do not repeat `metricId`, `currency`, `groupBy`, `from`/`to`, or `datePreset` on a widget when it already matches `reportContext`
- Do not put the global scope in each widget's `filterCel` instead of `reportContext.conditionsCel`
- Do not confuse ownership (`teamId` / `visibility`) with query scope (`scopeId` / `conditionsCel` / `filterCel`)
- Do not use different widget *content* for preview vs create — same `reportContext` + DIGEST fields — but the key differs: preview = `widget`, create/update = `widgets[]`
- Do not pass `widgets` to `preview_report_widget` or a singular `widget` to `create_report` (Zod strict rejects both)
- Do not send dashboard `textContent` on a report TEXT widget — the field is `contentMarkdown` with `type: "TEXT"`
- Do not skip preview before create when DIGEST is in the mix
- Do not skip confirming the DIGEST tree path after `suggest_groupby` when delivering NOW/SCHEDULED (chat-only Explain may preview in the same turn)
- Do not substitute destinations silently
- Do not use `query` + `compare` as a substitute for Explain’s DIGEST preview on the first answer
- Do not rebuild DIGEST movers as explorer tables or a canvas before presenting preview fields
- Do not schedule a weekly `LAST_WEEK` report for Monday (`weekday: 1`) — cost data is ~2 days late; use Tuesday (`weekday: 2`) with `LAST_WEEK`, or keep Monday and switch to `TRAILING_7_DAYS`
- Do not generate the deprecated `context` alias or send it together with `reportContext`
## Related skills
- `dashboards` — when the user wants an interactive dashboard instead of a delivered report (same context-first inheritance model; also uses `suggest_groupby`)
- `query` — **after** DIGEST preview, drill a user-named node (`filterCel`); not the primary tool for “what changed last month”
- `recipes` → `explain-period-change` — one-shot explain outcome card (preview-first DIGEST)
- `virtual-dimensions` — when a DIGEST hierarchy needs a custom axis that does not exist yet