# Normalized report data The renderer consumes normalized report data, not raw MCP responses. Build each semantic row by zipping `columns[].name` with the row values, then populate this schema. Ratios such as ROAS use multiplier units (`6.31` means `6.31×`); percentage fields use percentage points (`4.18` means `4.18%`). Money values use the account currency. ## Required shape ```json { "meta": { "report_date": "2026-08-19", "account_name": "Demo Commerce", "account_id": "demo-account-001", "currency": "MYR", "reporting_timezone": "Asia/Kuala_Lumpur", "period_label": "2026-08-19", "comparison_label": "Previous 7 complete days", "freshness_label": "2026-08-20 14:48 MYT", "attribution_label": "TikTok GMV Max native attribution", "status_label": "Cautiously positive" }, "summary": { "spend": 1837.84, "revenue": 11593.35, "orders": 663, "ctr_percent": null, "cpi": null, "roas": 6.31, "simple_roi_percent": 530.8, "cost_per_order": 2.77 }, "monitoring_note": "Optional partial-day or data-quality note.", "trend": [ {"label": "08/14", "roas": 6.45, "partial": false} ], "campaigns": { "winners": [ {"name": "Campaign name", "id": "external id", "status": "ENABLE", "spend": 10, "revenue": 80, "orders": 5, "roas": 8, "reason": "Balanced scale and efficiency"} ], "anomalies": [] }, "creative": { "row_count": 692, "coverage_percent": 75.2, "source_groups": [ {"label": "Affiliate / creator", "spend": 1232.55, "revenue": 7535.94, "orders": 463, "roas": 6.11}, {"label": "Account-owned", "spend": 150.2, "revenue": 121, "orders": 8, "roas": 0.81} ], "winners": [ {"name": "Creative title", "id": "creative id", "creator": "creator", "spend": 20, "revenue": 350, "orders": 20, "roas": 17.5, "clicks": 89, "product_ctr_percent": 7.0} ], "anomalies": [], "insights": ["Short evidence-backed attribution clue"] }, "recommendations": [ {"title": "Action title", "detail": "Observed evidence and bounded next step.", "confirmation": "required"} ], "manual_confirmations": [ {"action": "Pause or change budget", "required": true, "reason": "Changes live delivery"} ], "notes": ["Attribution and unavailable-data caveats"] } ``` ## Completion rules - `recommendations` contains exactly three items. `confirmation` is `required`, `partial`, or `none`. - Winner and anomaly tables should each contain three to five rows when sufficient data exists. Keep low-sample rows but identify the sample limitation in `reason`. - Missing CTR/CPI/ROI inputs are JSON `null`. Never encode missing data as numeric zero. - `creative.source_groups` may be empty when creator identity is unavailable. Keep `creative.insights` evidence-based and avoid causal language. - `manual_confirmations` covers every recommended platform write. Read-only checks may use `required: false`. ## Runtime bounds All objects are strict. report_date must be a real ISO YYYY-MM-DD date; timezone an IANA timezone; currency a three-letter code. Text fields: maximum 500 characters (names/IDs/labels 160). Arrays: trend 7, each winner/anomaly table 5, source_groups 6, insights 4, notes and manual_confirmations 10. Metrics are finite numbers or null; counts are nonnegative integers. Campaign reason is required; creative reason is optional for sample caveats. Required/partial recommendations need a required manual confirmation entry. Reports require non-null spend and at least one other observed summary metric.