- Brand
- Kreel
- Category
- Data & Analytics
- Primary Subcategory
- Ad Campaign Analytics & Reporting
Integration details
Description
Kreel connects ChatGPT to normalized ecommerce performance data from Shopify, Klaviyo, Meta Ads, and Google Ads. Users ask about revenue, ROAS, campaigns, creatives, email performance, and anomalies across their connected brands, keep a durable brand knowledge base, and publish hosted reports on request.
- Integration type
- Plugin
- Verification status
- Not applicable
- Platform
- ChatGPT
- Primary Subcategory
- Ad Campaign Analytics & Reporting
- Secondary Subcategories
- None listed
- Brand
- Kreel
- Access
- Account required
- First tracked
- 2026-07-31
- Tool count
- 45
- Market
- US
The Primary Subcategory used for this profile’s headline score.
Other Subcategories where the Integration is listed.
Your score is coming
ChatGPT now suggests Plugins on its own when they match a user's request.Your Plugin Discovery Score measures how often yours appears, and it will show here as soon as it’s ready.
What discovery looks like

Get alerts for Kreel
Get updates when Kreel’s Discoverability Score or category rank changes.
Competing in ChatGPT Ad Campaign Analytics & Reporting
View Category45 tools agents can invoke
Soft-archive a decided experiment. Its result and learning remain available.
archive_experiment
Soft-archive a learning (drops it from the active set and the cap count). Args: brand: Brand name or id. Required when you can reach more than one brand.
archive_learning
Compare a metric across subjects, optionally against a derived period. A single subject is supported when comparison is supplied. Every subject carries `revenue_kind`, matching get_performance's vocabulary: `store_net` (a Shopify account subject — refund-correct NET store revenue, so it reconciles digit-for-digit with get_performance), `platform_attributed` (an ad/email account or any campaign subject — provider-reported value under its own attribution model and click/view windows, not necessarily last-click; equal revenue kinds do not imply equal attribution settings). A platform='all' account subject is `store_net` too: store-net revenue, and for `roas` MER (store net / total Meta+Google+TikTok ad spend, the same figure as get_performance's `summary.mer`), carrying `basis: "mer"`. When `metric` is `revenue`, `roas`, `orders` or `cpa` and the subjects mix revenue kinds, a top-level `notices` entry flags that the numbers are not directly comparable. Args: brand: Brand name or id to query. Required when you can reach more than one brand; optional (auto-selected) when you can reach exactly one. Call list_brands to see options. account: Optional Connected Account or list of them (each a display name, platform account id, Kreel id, or a saved Account Group name, which resolves to its member set; several values union) applied to EVERY subject, so subjects stay comparable. Omit it to compare across the blend of all the brand's accounts. An account the brand does not hold returns {"error": "unknown_account", "available_accounts": [...]}. subjects: list of {"label", "platform", "entity_type", "id"?}. entity_type = "account" compares full channels; "campaign" requires an "id" (platform_campaign_id). "email_campaign" and "flow" accept Klaviyo ids, optional account_id and send_channel (email/sms), with platform "klaviyo" or omitted. Rates are unique/delivered fractions. Campaign cohort comparison is group-analysis with source_type=campaign. `subject.platform` is optional and defaults to 'all': for an account subject, the whole brand on one store basis, not a sum of channels. For platform-reported revenue, ROAS or conversions, name the platform. Campaign subjects support 'meta', 'google', 'tiktok', or 'all'; 'all' scans every paid-ad platform. Campaign lookup scans only the top ~1000-by-spend campaigns and matches by id; set the subject's `platform` to avoid cross-platform id collisions, and 'no data' may mean truncated rather than absent. GA4 subjects (platform "ga4", never mixed with other platforms) are the website or one segment: entity_type "website", or "channel_group" | "traffic_source" (source / medium) | "device" | "landing_page" with `id` = that value, plus optional GA4 `filters` (channel_group, source_medium, device_category, landing_page). GA4 metrics: sessions | engaged_sessions | engagement_rate | purchase_session_rate | ecommerce_purchases; each subject lists its GA4 warning codes and a comparison is withheld unless stored GA4 data covers the whole comparison window. comparison: Optional {"mode": "previous" | "year" | "custom", "start"?, "end"?}. Derives period-over-period values with strict coverage. delta_pct is (current - previous) / previous, rounded to 4 places (0.1 means 10%). metric: spend | revenue | roas | cpa | orders | conversions | ctr | delivered | open_rate | click_rate | revenue_per_recipient. For account subjects, `orders` and the denominator of `cpa` use Shopify store orders for Shopify and platform conversions for Meta, Google, TikTok, and Klaviyo. A platform='all' account subject uses Shopify store orders, and its `cpa` is total paid-ad spend / store orders (`basis: "ad_spend_per_store_order"`); its `conversions` is `value: null` with a reason, because platform conversions overlap and are never summed across platforms. A platform='all' subject with no Shopify store in the account scope returns `value: null` with a reason for revenue, roas, orders and cpa. For campaign subjects, `orders` equals conversions (no true order count exists at campaign level). window: {"days": 7} OR {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}. `window` defaults to the last 7 days; day-count windows end yesterday, not today. sales_channels: Optional normalized Shopify `source_name` allowlist. This applies to Shopify-derived revenue and orders for account subjects whose platform is "shopify" or "all", and wins over the brand exclusion policy. Ad and email subjects remain unfiltered. A platform='all' account subject therefore uses the filtered store revenue and orders over full paid-ad spend, the same as get_performance's MER. Use "__unknown__" for orders without source_name. Unknown values return an error with observed values. Omit the argument to use the brand policy; [] is invalid.
compare
Create a custom dashboard of DSL tiles on the Kreel Dashboards page. The dashboard is stored in Kreel and only Kreel members can see it; nothing is published outside the Workspace or sent to another service. A tile is {kind, title, dsl, layout: {x, y, w, h}, display?: {metric?, format?}} on a 12-column grid (x + w <= 12). kind is: - kpi: draws the first row's `display.metric`; group or filter the query to one row (e.g. group_by ["channel"] with a channel filter), no date grouping; - timeseries: selects and groups by the table's date column, plus at most one series column; - bar: groups by exactly one non-date column; - table: any query, shown as rows. `dsl` is the `query` tool's DSL (validated the same way, no cursor); the dashboard range replaces any date filter it has. A tile that sums store and platform-attributed revenue together — the #289 mixed-basis notice `query` attaches — is rejected, and nothing is saved. At most 24 tiles. Needs the `write` scope and the member role. Private dashboards are yours; `brand` shares one with the Brand's Kreel members and `workspace` makes a template for every Brand of your Workspace. Args: name: Dashboard name (at most 120 characters). tiles: The tiles, in reading order. brand: Brand name or id. Required when you can reach more than one brand. visibility: "private" (default) | "brand" | "workspace". range: {preset: yesterday|7d|30d|90d|mtd|custom, days, start_date?, end_date?}; default the last 30 days. description: Optional one-line description.
create_dashboard
Flag channel and campaign metric changes with structured evidence. Inventory stockout_risk and dead_stock use canonical products and trailing 28-day velocity. Linked ACTIVE Meta spend is best-effort, Meta only and tagger-dependent; inspect inventory_metadata.advertising_coverage for partial coverage. Klaviyo campaign bounce/spam rates compare with preceding account sends: at least 1,000 deliveries and 5 bounces / 3 complaints, +0.005 / +0.0005 absolute rate increases, and 25% / 50% relative warn / critical increases. Measured zero-baseline spikes are new_activity; flow gaps are omitted. Also detects product/variant high_return_rate against the recent store baseline (30 sold, 5 refunded, 100 store units; +5pp and 2x warn/3x critical). Weekly overdue_share (shopify_store, whole brand only, not with `account`): the latest weekly RFM run's overdue customers / scored customers against the run exactly one week earlier. Warn above a 20% relative rise, critical above 40%; no alert when either week is missing or the prior share is 0. Evidence has basis "weekly_snapshot", week_start and prior_week_start. Linked active Meta spend is canonical-product evidence over the last seven completed days, separate from the return window. The tool evaluates spend, revenue, orders, ROAS, CPA, CTR, and CPM. Additive metrics (spend, revenue, orders) are compared as calendar-day averages; ratios (roas, cpa, ctr, cpm) are recomputed from summed bases, never averaged. Each alert's evidence carries ``basis``: ``"daily_avg"`` for the additive metrics or ``"ratio"`` for the ratio metrics, alongside the ``window_value``/``baseline_value`` keys (unchanged). Channel ``orders`` means store orders for Shopify and platform conversions for attributed channels; campaign ``orders`` also means platform conversions. Google values may be fractional. Results are capped at the 10 highest-severity alerts and ``truncated`` says whether more matched. Default warn / critical percent thresholds are: spend 25/50, revenue 20/40, orders 20/40, ROAS 20/40, CPA 25/50, CTR 20/40, and CPM 25/50. Direction is literal ``up`` or ``down``, not a claim that a movement is good or bad. A zero baseline is only flagged for spend at 50 account-currency units per day or orders at 5 per day. It is ``new_activity`` when the recent value crosses that floor and ``stopped`` when a baseline above the floor falls to zero. New activity has null ``pct_change``; stopped activity has -100. Twice the floor is critical. Undefined ratios and other zero-baseline metrics are skipped. Channel revenue is refund-correct store net for Shopify and platform-attributed value elsewhere. Campaign revenue is platform-attributed. Attribution models and click/view windows differ by provider; the `platform_attributed` label does not imply last-click attribution. Revenue and ROAS evidence carries ``revenue_kind``. Campaign evaluation is bounded to the top 20 by baseline spend, plus bounded current-only launches. Args: metrics: Optional metric allowlist, including days_of_cover, velocity and overdue_share. brand: Brand name or id to query. Required when more than one brand is reachable; optional when exactly one is reachable. Call list_brands to see options. account: Optional Connected Account or list of them (each a display name, platform account id, Kreel id, or a saved Account Group name, which resolves to its member set; several values union). Omit it to detect anomalies over the blend of every account; name one to detect them within that account alone, which is how a swing in one market stops being averaged away by the others. A brand with several accounts on one platform gets an `accounts` manifest naming what was folded in. An account the brand does not hold returns {"error": "unknown_account", "available_accounts": [...]}. window: Recent period, default {"days": 7}. baseline_window: Comparison period. Defaults to the 14 days immediately before ``window``. threshold: Optional backwards-compatible extra minimum percent change. It can make detection stricter but cannot lower the metric table. channels: Exact channel ids to include: meta_ads, google_ads, tiktok_ads, klaviyo_email, klaviyo_sms, or shopify_store. Any unrecognized id - alone or mixed with valid ones - returns a structured error {"error": ..., "valid_channels": [...]} instead of a silently filtered or unfiltered result. severity: Optional subset of "critical"/"warn" to include. Applied before the top-10 cap, so a window with more than 10 critical alerts no longer starves out every warning - call again with severity=["warn"] to retrieve them. Omitted (default) keeps all severities. An empty list is a structured error - severity must be a non-empty subset when provided, or omitted entirely to get all severities. An unrecognized value returns a structured error {"error": ..., "valid_severities": [...]}.
detect_anomalies
Return a deterministic, source-cited one-day brief on the Brand reporting calendar. Args: brand: Brand name or id to query. Required when the caller can reach more than one Brand. date: Optional YYYY-MM-DD Brand-local day. It must be before today. Omit it for yesterday. Each metric compares the day with the same weekday a week earlier and with the preceding 28 complete days (D-28 through D-1): money and counts against the 28-day total / 28 (``daily_average``), ratios against aggregate numerator / denominator (``aggregate_ratio``). A comparison whose history is incomplete is null and ``comparison`` gives the reason; it is never zero. ``profit`` has yesterday and month to date: refund-correct store net revenue, orders, AOV, ad spend, MER, CM1 and contribution after ads. Margin amounts use current unit costs, exclude tax and shipping, and are reported only at 80% cost coverage or more; otherwise the level carries its status and coverage and no amount. ``pacing`` and ``budget_pacing`` are month to date through the day. ``findings`` are the top five from the findings registry. ``freshness.stale`` flags a platform more than 3 days behind or unknown. ``edition.status`` is ``pending`` until that day's materialization has completed; delivered digests wait for it. Store and attributed revenue are different bases and must not be added.
get_daily_brief
Return a deterministic, source-cited seven-day growth brief. Inventory includes the top three stockout risks, dead stock and coverage counts. Current catalog stock uses velocity ending at the brief end, capped at yesterday; advertising is partial, Meta only and tagger-dependent. Args: brand: Brand name or id to query. Required when the caller can reach more than one Brand. week_ending: Optional YYYY-MM-DD final day. It must be before today. Omit it for the last seven complete reporting-calendar days. Includes experiments due for review and verdicts landed in the window, with source links to the immutable ledger. Reads never trigger review. Includes creative_fatigue: independent Meta alerts, all-ad counts and daily spend, up to ten examples and resolvable source citations. Diagnostic windows end no later than yesterday; missing data is disclosed. The response includes refund-correct store revenue, refunds, paid spend, MER, per-platform ROAS in `metrics.platform_roas`, each labelled by its source (platform claims overlap and are never summed), CAC based on Shopify first orders, Klaviyo attributed revenue, campaign movers, threshold anomalies, fixed-rule next actions, and a citation for every source. Store and attributed revenue are different bases and must not be added.
get_weekly_growth_brief
Read one dashboard with every tile's numbers — what the Dashboards page shows. Returns `dashboard` (name, visibility, range and tiles), the resolved `window` on the brand's reporting calendar, and `tiles[]`: per tile its `dsl` as run (the tile's DSL with the dashboard range on the date column; passing it to `query` returns the same rows), `applied` / `not_applied`, and `result` ({meta, notices, rows} as `query` returns) or `error`. Args: dashboard_id: The dashboard id (from list_dashboards or a dashboard link). brand: Brand name or id. Required when you can reach more than one brand. A workspace template runs against this brand. range: Optional range for this read only, e.g. {"preset": "7d", "days": 7} or {"preset": "custom", "days": 14, "start_date": "2026-09-01", "end_date": "2026-09-14"}. Omit for the dashboard's saved range.
get_dashboard
Read one saved view or Ask snapshot with the numbers its page shows. Returns `view` (id, name, page, visibility, `ephemeral`, `expires_at` and the stored `state`), the resolved `window` and `comparison` dates on the brand's reporting calendar, `accounts` (the view's account filter and the accounts it selects), `data` (the page's own payload from the page's builders: Email → `email_performance`, plus on the Overview tab `email_top_flows` (its Top flows list), `email_klaviyo_status` (the account totals), `email_time_series` and `email_deliverability`, on the Campaigns or Flows tab `email_campaigns` / `email_flows` and `email_group_analysis` under the page's channel, tag, search, draft and selection filters, on the Flows tab also `email_flow_coverage` and `email_flow_benchmarks` (their own last 90 complete days, every Klaviyo account, pinned as of the Ask; medians over the Brands you reach), and on the Audience tab `email_audience_growth` (over its own pinned weeks), `email_lists` and `email_segments` at the pages on screen and `selected_audience` (the list or segment the growth chart showed, and its `view`); Creative → `creative_summary` on the Overview tab, `creative_attributes` on Attributes (with `selected_attribute` for an attribute row Ask, and `creative_combinations` with `selected_combination` for a Winning combinations row Ask), `creative_lifecycle` on Lifecycle and `creative_gallery` on Gallery (its sort, filter bar and "Needs attention" order applied), stage classification pinned to the day asked (`view.state.pinned.page_window`); Performance → `performance`; Paid Ads → `ads`, `paid_ads_table` and `budget_pacing`; Website → `website_summary` and its four breakdowns; Margin → `profit_ladder`, `contribution_target` (the monthly target in each month cell, as when asked) and `products`; Cohorts → `cohorts`, `cohorts_cac`, `cohorts_repeat` and `cohort_lens` (metric, basis, display, view, outline, added values, best-dimension ranking); Products, Countries, Repeat, Inventory, Segments, a custom dashboard and a Portfolio brand row likewise; long tables return their top 50 rows plus the selected one, and a table the user had searched returns only the rows the search left on screen, with `<rows>_searched` and its totals whole) and `equivalent_args`: the get_performance arguments reproducing the view's window and scope, with `not_applied` naming what does not map. An Ask snapshot (from the dashboard's "Ask about this view") also carries `context` (the surface, metric, row subject or finding the user asked about) and `selected` (that row as the page's table shows it). Its dates are pinned: they are the windows on screen when the user asked, however late you read it (`pinned: true`). A finding's own detector window is in `context.finding`, separate from the page's dates. Undated pages name their own window in `page_window`; Repeat's first-order cohort window is pinned too (`view.state.pinned.page_window`), and Cohorts are read as observed on the day asked (`page_window.observed_on`: that period stays partial), as is a Portfolio row's payback line (its latest closed cohort stays the one shown after a month midnight). A custom dashboard runs its tiles as they were when the user asked, not as since edited (the revision on screen: a tile saved over before the Ask is refused, never captured); a table tile row Ask adds `selected_tile_row`, the row found again by its grouped values (`context.tile_row.key`). An Ask about a fixed aggregate (a Margin ladder line, a Cohort matrix cell, an RFM grid or migration cell, a group metric row, a deliverability provider, a platform card, a lifecycle stage) names it in `context.aggregate` by the page's own identifiers, and `selected.row` is that aggregate as the page data shows it. A customer segment Ask reads through the segment service: `segment_metrics` and `segment_history` (through `context.segment.through_week`, with `selected_segment_week` the row's latest week; empty when `context.segment.history_empty`, a row asked before its first weekly run) for a saved segment at the version on screen, and `segment_preview` with its `segment_rule` for an unsaved builder rule, over `context.segment.revenue_window`, with recency measured from `context.segment.observed_on`, the day asked; an "Overlap with RFM segments" row is `selected`. A Paid Ads snapshot keeps pacing's own anchor (`page_window.pacing_as_of`, or `context.pacing.as_of` for a budget pacing Ask) and the weekly charts' twelve weeks (`page_window.weekly`): `budget_pacing` re-reads the plan at that anchor, and once it is no longer yesterday the table's budget-owner pace cells are the ones captured at the Ask (`pacing_source`), since budgets are current snapshots. A pacing row Ask adds `selected_pacing`, that row's cell as the table returns it (`source`). An Ask snapshot lasts 7 days; afterwards this returns {"error": "view_expired", "message": ...}. A view you cannot see on this brand returns {"error": "view_not_found", ...}. Args: view_id: The saved view or snapshot id (from list_views, a dashboard `?view=` link, or an Ask handoff). brand: Brand name or id. Required when you can reach more than one brand.
get_view
One run of a scheduled agent: status and reason, attempts, duration, inputs, the steps it ran, its output summary, the Report it published and what each destination did. A held draft includes its rendered document.
get_agent_run
Return identity, operational metadata, and the saved brand profile. Top-level fields: id, name, currency, timezone, revenue_basis, `sales_channels`, is_active, onboarded, and `connections` (each connected platform with account name, active flag, and whether it last errored) — the context needed to interpret figures correctly (currency on revenue, timezone on date boundaries, which platforms have data). `timezone` is the resolved reporting timezone (the Shopify shop's own timezone while Shopify is connected, else the brand profile's saved timezone, else UTC) - the same value `readiness.expected_data_through` and every daily table's day boundaries are computed from. It is not the raw, unbackfilled profile setting. `connections` includes only currently-active platforms; disconnected ones are omitted (not shown with is_active=false), so per-connection `is_active` is always true. `account_groups` lists the Brand's saved Account Groups — named cross-platform sets of Connected Accounts ("EU", "US") — as {name, id, member_count, platforms}. A group name is a valid `account` argument on every read tool and resolves to its member set; the result's accounts manifest then names the group. Empty when the brand has none. Each connection carries freshness signals: `data_through` is the latest activity date represented in queryable rows, `materialized_at` is when those query rows were last rebuilt, `materialized_from` and `materialized_through` identify the rebuilt date window, and `last_synced_at` is when the last completed performance-relevant Sync Run finished. These can differ when a sync and materialization find no newer activity. `data_through` is measured from a single representative channel per platform (Shopify = shopify_store), not every channel that platform may feed. With several Connected Accounts on a platform each is judged on its own runs and the worst is reported: `last_synced_at` is the least recently synced account's (null while any account has never completed a run), and `last_sync_status` is the latest performance-relevant Sync Run's status of the worst account, including failures, so a sibling's later success never hides one. `last_sync_partial` is true when that run completed but recorded slice errors. `materialized_at` advances after a successful full materialization pass even when sparse activity produces no query rows. `sources` is the per-source freshness verdict the dashboard shows in its report header, one entry per connected source (plus disconnected sources whose retained data still feeds reports): `platform`, `label`, `data_through`, `last_success_at` (latest fully successful performance sync, UTC), `sla_hours`, `status` (`fresh` | `stale` | `failed` | `reauth`, worst first: reauth > failed > stale > fresh; or `disconnected` for retained data from a source with no active connection, which is neither fresh nor a fault) and `reason` (`sync_failed`, `sync_partial`, `past_sla`, `never_synced`, `reauth_required`, `token_expired`, `needs_selection`, `token_error`, `disconnected`; `demo` on a demo Brand's seeded sources; otherwise null when fresh). Quote these when stating freshness so your answer matches the dashboard. `readiness` is the mandatory preflight for performance analysis. Call this tool before `get_performance`, then disclose every warning before interpreting rows. `ready` is false when there are no connections or a connection has missing data, source data not synced or query data not materialized within `freshness_target_hours`, the materialized window does not cover the documented 30-day analysis range, a failed or partial latest sync, or a connection error. Sparse platform activity is not considered stale when both the source sync and query surface refresh are within the freshness target. `expected_data_through` is yesterday in the Brand's reporting timezone. Machine-readable warning codes are `no_connections`, `missing_data`, `stale_data`, `sync_failed`, `sync_partial`, and `connection_error`. `data_coverage` reports the queryable historical window at reporting-table, platform, and day/month grain. `observed_periods`, `span_periods`, and `has_period_gaps` distinguish a continuous period range from sparse history. Applicable connected-platform slices with no rows are explicit `no_data`. `completeness` is always `unknown` and never promoted to `incomplete` or `complete`: row-date gaps are activity gaps, not provably ingestion gaps (materializers only write rows for periods that had activity, so a legitimately quiet day produces the same gap shape as a failed sync), and a gap-free range does not prove every expected entity or monetary row was ingested either. A trustworthy completeness verdict would need the durable sync/materialization coverage ledger, not row presence - that is out of scope for this field. Use `has_period_gaps` to judge sparseness, but never read `completeness` as a completeness guarantee in either direction. `sales_channels.policy` is the brand's persisted exclusion list. Newly discovered Shopify channels are included by default. `sales_channels.available` lists observed normalized Shopify source_name values with labels and all-time order counts. Pass one or more of those `channel` identifiers to the `sales_channels` argument on get_performance account/product queries or compare for a per-call allowlist override. The reserved "__unknown__" identifier selects orders without source_name. Ad-platform and email metrics are never filtered by this Shopify policy. Brand Knowledge Base — anchor every recommendation in this: - `context`: the Brand Profile typed anchors — vertical, price_tier (value/mid/premium/luxury), margin_band (lean/moderate/healthy), target_roas, target_cpa, target_aov, and the monthly target set: monthly_revenue_target, monthly_spend_budget, target_mer, monthly_new_customers_target, target_new_customer_cac, monthly_contribution_target (contribution after ads per month on the profit ladder; not paced, may be negative), and lapse_threshold_days (the customer-mix gap after which a returning order is `reactivated`; null means the default 180), pacing_tolerance_pct (the budget pacing tolerance, see get_budget_pacing; null means the default 10%). Individual fields may be null; check before use. - `pace`: Target pace against the budget plan and monthly targets, month to date through yesterday on the brand's reporting calendar. Method is straight-line: expected to date = monthly target x days elapsed / days in month (no seasonality weighting). One entry per set target: `revenue` and `new_customers` carry `expected_to_date`, `pace_pct`, `gap`, `projected` and status behind (<100% of pace) / on_pace (<105%) / ahead; `spend_budget` is neutral (`over` above 105% of pace, else `within`); `mer` (`attainment_pct`) and `new_customer_cac` (`over_pct`) are month-to-date met/missed. Thresholds apply to the percentage as displayed (whole percent, half-up), so 99.6% of pace is on_pace. Spend budget, MER and CAC are `not_scored` with a `reason` while a paid source is failed, stale or backfilling, or the denominator is zero; revenue, new customers, MER and CAC are `not_scored` (`no_store_source`, `store_source_backfilling`) while no Shopify store is in scope. They never read as behind against a 0. `summary` holds one cited line per metric. When the active budget plan covers the month, each metric with a plan value paces against it (`source: "plan"`; `pace.source`, `plan_version`, `plan_currency`); the rest keep the monthly target (`source: "targets"`). A plan `spend_budget` sums only the ad `platforms` it names and compares their spend. A plan money line in another currency than the Base currency is `not_scored` (`plan_currency_mismatch`); a plan value of 0 is `not_scored` (`zero_plan`). `pace` is null when neither a plan value nor a monthly target is set for the month - never invent one. - `sections`: prose markdown documents keyed by `key` (positioning, icp, competitive_landscape, brand_voice) — the brand's narrative and voice. - `competitors`: the brand's named rivals (name, handle, differentiator). - `learnings`: durable insights (id, topic, content, evidence) the brand has accumulated across sessions. `learning_cap` is `{active, cap}`. - `decisions`: the agent decision queue (ADR-0023). `open` lists pending proposals and `recent` the 20 latest others with `status`, `rejection_reason` and `verdict`; `open_count` counts pending ones against `cap`. Never re-propose an open decision (supersede it instead) or a rejected one without addressing its `rejection_reason`. - `rfm`: the last weekly RFM run - `last_calculated_at`, `customers_scored` (Brand identities, each person once across stores) and `snapshot_weeks` (retained weekly snapshots, which bound the migration horizons). Null when RFM has never run. Segment aggregates are the query tables `rfm_segment_summary` and `rfm_migration`. `is_empty` is true when the profile, sections, competitors and learnings are all unset — point the user at onboarding (the `/onboard-brand` skill). Args: brand: Brand name or id to query. Required when you can reach more than one brand; optional (auto-selected) when you can reach exactly one. Call list_brands to see options.
get_brand_context
Ad spend pacing against the monthly budget plan and the platform budgets. The month is the Brand-calendar month containing `as_of` (default: yesterday). Read-only: every recommendation is advisory and Kreel never changes a budget. `platforms` - one entry per ad platform (Meta, Google, TikTok) against its line of the active budget plan (`spend_meta` / `spend_google` / `spend_tiktok`, see get_plan), in the Base currency with spend converted per row date: `mtd_spend`, `plan`, `expected_to_date` (the linear pace line, plan x days elapsed / days in month), `pace_pct`, `projected` month-end, `deviation_pct`, `required_daily_spend` and `daily_change_needed` (the run-rate change that lands on plan). The projection method is always "Linear from trailing 7-day average": MTD + the average of the seven days ending `as_of` x days left. `status` is on_pace, over_pace or under_pace (projected beyond the Brand's `tolerance_pct`, default 10, strictly), no_plan (blank plan cell: create one) or not_scored with `reason` (a failed, stale or backfilling source, missing FX, a plan in another currency, or an account lens that covers only part of the platform). Accounts under a platform are contributions with `share_of_plan_pct`, never per-account plans. `entities` - each active budget owner (a campaign with its own budget, otherwise its budget-bearing ad sets / ad groups; a Google shared budget once across its `member_campaign_ids`) in the account's own currency. Daily budgets carry `yesterday_pct` and `trailing_7d_pct`; `flag` is under_delivering below 70% or overdelivering above 100% (exactly 70 and 100 do not flag). Lifetime budgets carry `budget_remaining` (reported or derived), `days_remaining` to `schedule_end` and `projected_vs_remaining_pct`. Budgets are current snapshots (`snapshot_at`), so an `as_of` other than yesterday returns no entities (`entity_status: historical_anchor`). An unavailable entity names its `unavailable_reason`. Args: as_of: A completed day "YYYY-MM-DD" on the Brand calendar; default yesterday. A later day is an error. account: Optional Connected Account(s) or Account Group, as in get_performance; an unknown account returns {"error": "unknown_account", "available_accounts": [...]}. brand: Brand name or id. Required when more than one brand is reachable.
get_budget_pacing
The Brand's monthly budget plan: 12 months x lines, in the Base currency. Lines: spend_meta, spend_google, spend_tiktok, other_marketing, net_revenue (money), new_customers (count), mer (ratio), cm1 (money). Each line's `values` maps every "YYYY-MM" of the version to an amount or null (null = no plan for that cell, never 0). Returns the active version unless `version` is given, every version's summary, and `diff` against the previous version. `{"plan": null}` when the Brand has no plan. For plan vs actual per month and line, query the `plan_vs_actual` table.
get_plan
Fetch creative performance for Meta ads, TikTok ads or Google assets. Meta-only naming: `naming` contains strictly parsed, case-sensitive values (UGC and ugc differ), or null. Configured fields support filters/group_by; missing values form an `unmatched` bucket. `naming_matched` filters coverage. `rank_by` ranks rollups by a chosen metric; `min_spend` sets a Base-currency spend floor. Incomplete/undefined groups are excluded and counted; cost metrics rank ascending, other metrics descending. `limit` caps ranked groups. Google has no naming schema, filters, or group_by support. This is the tool for creative analysis: each creative carries its delivery **performance over a window** (spend/impressions/roas/ctr/cpa…) plus fatigue diagnostics and Kreel's AI creative tags, so you can answer "which hooks/formats convert" and "what's fatiguing" in one call without joining tables yourself. ⚠️ **Media URLs and ad copy require `fields="all"`.** The default `fields="core"` is token-safe and returns NO media/copy and NO Gemini prose — see `fields` below. Canonical Meta hook_rate = thumbstop = video_3_sec_views / impressions; thruplay_hold_rate = thruplays / video_3_sec_views (15 seconds or completion). These are four-decimal fractions, null for missing inputs or nonpositive denominators. Asset grouping uses confirmed image hashes only, falling back to account-scoped creative identity, then ad identity. Legacy fatigue uses 30-second video_views for hook and p100 / video_views for hold. **Google v1 is metrics-only at asset + field-type grain.** It covers RSA and Demand Gen assets through `ad_group_ad_asset_view`, and PMax assets through `asset_group_asset`. Rows include asset id, type, name/text when Google supplies it, field_type, performance_label, impressions, clicks, ctr, conversions, conversion_value, and cost. The envelope and each row echo the selected Google customer id so asset identities stay account-scoped. Google v1 has no Gemini tags, media URLs, fatigue, filters, or group_by. Pass `fields="core"`; `tags` and `all` return an explicit not-available error rather than silently degrading. When the Google page comes back empty, the envelope adds an `empty_reason` so a zero result is never ambiguous: "asset_metrics_not_synced" (the per-asset sync has never populated this account - no rows for the selected customer on any date, e.g. the pipeline hasn't run or a shared-quota hit kept coming up empty) vs "no_rows_in_window" (rows exist for the account, just none delivered in the requested window - widen `date_range`). **TikTok is ad-grain creative parity with Meta** (#642 D4): each delivered TikTok ad that has a fetchable asset is one row carrying `creative_id` (its video_id, else first image_id - reuse across ads shares one id and one stored copy), `asset_kind`, `media_type`, the shared `hook_type` / `format_type` tags, `intelligence` (`fields="tags"`), Kreel-signed `image_url` / `video_url` plus `body` / `call_to_action` / `landing_page_url` (`fields="all"`), and spend / impressions / clicks / `purchases` (website purchases) / `revenue` (their TikTok-attributed value) / `cta_conversions` / `vta_conversions` with derived roas / cpa / ctr / cpm / cpc / conversion_rate and a per-row `attribution` block, exactly as `get_performance(platform= "tiktok", entity_type="ad")` reports them; a `semantics` block restates the definitions. Delivered ads with NO fetchable asset (Spark Ads without a video_id, LIVE_CONTENT, CATALOG_CAROUSEL) are excluded from the rows and counted, with their spend, in `without_creative` - `total_creative_spend` covers only the rows a creative ranking can see. Filters: ad_id, campaign_id, adset_id (= adgroup_id), status (the raw TikTok operation_status switch, e.g. "ENABLE" - advertiser-enabled, NOT guaranteed delivery). Each row carries both `status` (that switch) and `secondary_status` (TikTok's effective delivery state, as `get_entities` reports it): an ENABLE ad under a paused campaign / ad group reads status "ENABLE", secondary_status "AD_STATUS_CAMPAIGN_DISABLE" / "AD_STATUS_ADGROUP_DISABLE" and lifecycle_stage "paused" - lifecycle is judged on the effective state. TikTok also accepts the shared AI tag and `custom:<key>` filters (see `filters` below). group_by: hook_type, format_type, media_type, the seven taxonomy v2 tags and `custom:<key>` (rollups carry the same measures; `rank_by` accepts roas / cpa / ctr / cpm / cpc / conversion_rate / spend / revenue / purchases). `lifecycle_stage` / `lifecycle_reason` / `lifecycle_bar` / `lifecycle_bar_source` come from the Brand's TikTok stage-rule row (per-platform, #466) over the same fixed 30-day window as Meta's; the Meta-only `fatigue` block is absent, not null. Do not compare raw TikTok and Meta ROAS: the platforms attribute differently. **Window-scoped & delivered-only:** results are driven by delivery in `date_range` (default last 7 days) — a creative that didn't spend in the window is not returned. To browse the whole active library, pass a wide window (e.g. `{"days": 365}`). Metrics match `get_performance(entity_type="ad")` digit-for-digit for the same window. **Two shapes:** - per-creative (default): one row per creative, spend desc. - rollup (`group_by` set): blended performance per tag dimension (hook_type, format_type, the v2 tags, `custom:<key>`, Meta `asset`) — ratios recomputed from summed base measures (never averaged), with a `creative_count` and an `untagged` bucket so spend reconciles. Returned under a `groups` key; media/copy/`fields`/fatigue are not applicable. Separate from get_entities because creative responses are token-heavy (media URLs + copy bodies) and have per-asset metadata. Each creative returns (in every `fields` tier from `core` up): - spend / impressions / clicks / reach / purchases / revenue and derived roas / cpa / ctr / cpm / cpc / conversion_rate over `date_range` (each ratio omitted when its denominator is 0). Creative revenue/roas/purchases are Meta-attributed (platform-reported, attribution-window dependent), not Shopify-verified store revenue. - mid-funnel counts add_to_cart / initiate_checkout (their `*_value` totals only when nonzero) — check ATCs before killing a zero-purchase ad. Meta-attributed, like purchases. leads / lead_value appear only when nonzero (most brands run none). - the video funnel, when the creative is a video (per its metadata) or the window saw video delivery: video_views (Meta 30-second views, not 3-second) + video_views_p25/p50/p75/p95/p100 percentile completions. NOTE the two use DIFFERENT measurement bases: video_views counts plays reaching 30 continuous seconds (an absolute time threshold - or the full length for videos shorter than 30s), while video_views_pXX count plays reaching XX% of the video's OWN length (a relative threshold, no 30-second floor). For a sub-30s creative - most short-form paid video - clearing 95% of a short clip is a lower bar than 30 continuous seconds, so video_views_pXX routinely EXCEEDS video_views. That is expected Meta reporting for short video, not a data defect. A delivered video nobody watched shows measured zeros; static creatives omit the group entirely - a structural gap, not a measured 0. These also blend per group in rollup mode. - creative_id — the Meta creative behind the ad. The same asset reused across ads/geos shares one creative_id, so join duplicates on it instead of matching ad names. Null until structure sync captures it. - first_delivery / age_days — first delivered date over the full synced history (not `date_range`) and days since, for launch-cohort reads. - landing_page_views / video_3_sec_views / thruplays / video_avg_watch_time_sec (unweighted mean of per-day averages; per-creative rows only, not rollups) — the counts appear only when EVERY contributing row in the window carries them (synced after the KRE-279 extension); a window crossing the rollout boundary omits them instead of publishing a partial total. Absence means not fully synced (re-sync history to fill), never a measured 0. video_3_sec_views/impressions is the classic hook rate; the fatigue block's hook_rate stays video_views (30-sec) based. - quality_ranking / engagement_rate_ranking / conversion_rate_ranking — Meta's diagnostic rankings; per field, the latest value reported in the window. A key is absent when Meta never reported that field (below ~500 impressions or unsynced). - hook_type — AI-classified hook (e.g. problem_solution, testimonial, product_demo, fomo_urgency). - format_type — AI-classified format (e.g. static_image, carousel, video_ugc, video_branded). - taxonomy v2 AI tags (Meta and TikTok), each one fixed-vocabulary value or null: angle_category (the persuasion angle's category), offer_type, persona, talent_type, cta_type (the CTA the creative itself makes - NOT the platform button in `call_to_action`), visual_style, seasonality. Null until the tagger has analyzed the creative with taxonomy v2. - custom_tags — {key: value} for the Brand's own custom tag dimensions (the Brand's creative tag schema); a key is absent when untagged. - lifecycle_stage — testing / scaling / winner / fatiguing / kill / paused / dormant, from a fixed trailing 30-day window — deliberately a *different* window than `date_range`, so a creative can be a 30-day `winner` while showing 0 spend in a narrower `date_range` (a fatigue signal, not a contradiction). The five PERFORMANCE stages are defined by the brand's editable per-Brand stage rules in fixed precedence (Kill > Fatiguing > Scaling > Winner > Testing, first match wins; Testing is the residual), judged against a per-audience-temperature efficiency bar (cold/warm/retargeting each default to their own cohort's blended ROAS, overridable with an explicit target; unclassified/other temperatures use the account blended bar), so a performance stage is relative, not an absolute quality signal. Two values are NOT performance stages (ADR-0016): "paused" — the ad's effective status is not ACTIVE (a paused parent ad set/campaign pauses the ad: Meta's effective_status, TikTok's secondary_status); it is a status, never a stage, and "kill" is reserved for LIVE ads failing the kill rule. "dormant" — the ad delivered inside `date_range` but has no row in the trailing 30-day window (it ran and stopped); it is NOT kill. Always set (results are delivered-only, so "never delivered" can't occur). "kill" replaces the retired "dead". - lifecycle_bar / lifecycle_bar_source — the exact efficiency bar this creative's stage was judged against and where it came from ("cohort_default" | "explicit_target" | "account_fallback"), so an agent reads the same bar the dashboard board prints. Both null for a "dormant" creative (outside the classification window, no bar resolved). - lifecycle_reason - WHY a `kill` creative is kill; null for every other stage (including "paused" and "dormant", which are self-describing: a Meta row's `status` and a TikTok row's `secondary_status` show the non-active delivery state behind "paused"). Kill has ONE meaning now: "underperforming" — still ACTIVE and delivering, but its recent AND cumulative ROAS are both below the brand's kill% of the efficiency bar (a deliberate "should be killed" verdict, NOT a delivery-state fact). An "underperforming" creative may still be actively spending. Also null on a `kill` creative whose ad metadata is missing (performance rows survive a deleted Meta ad row) - status is unknown there, so it is classified purely on performance, never assumed paused. - fatigue.alert — independent deterministic per-ad rule in every field tier: CPC rises >=25% AND measured three-second hook rate or ROAS falls >=20%, subject to current brand fatigue_alert_* settings and both-window floors. Compares seven days ending at requested end (capped at yesterday) with prior seven; echoes both windows, coverage, thresholds and inputs. Distinct from lifecycle stages and legacy descriptive trends. Frequency is contextual only. - fatigue — diagnostics over `date_range`: window scalars hook_rate (video_views/impressions), hold_rate (video_views_p100/video_views), ctr; for non-video creatives (no video_views) hold_rate is null - not-applicable, not missing data - while hook_rate reads a real 0; and `trend` (first-half vs second-half) for ctr/hook_rate/frequency, each {first_half, second_half, delta_pct}. `trend` is null when the window has < 2 days/side. NOTE: frequency is trend-only — there is no cumulative window frequency scalar, by design (reach is unique users and doesn't sum across days, so Σimpressions/Σreach would be wrong — ADR-0007). With `fields="tags"` each creative additionally carries `intelligence` — Gemini per-creative analysis: description, hook_summary (the literal opening hook), angle (the persuasion strategy), first_3_seconds, spoken_or_onscreen_text, pacing. With `fields="all"` it additionally carries image_url / video_url / headline / body / call_to_action — the asset and copy. `image_url` / `video_url` come in two forms, and only one is durable at publish: - **Kreel signed URL** (`…/storage/v1/object/sign/creative-assets/…`) — returned when the own-the-asset pipeline has stored the media (KRE-151). This is the **durable-at-publish source `publish_report` freezes**: reference it directly in report HTML and `publish_report` snapshots it into the report and serves it permanently, so its ~1-hour signing only needs to hold at publish time. - **Meta CDN URL** (a `…fbcdn.net…` / non-Kreel link) — the fallback returned when Kreel has not stored that creative's media yet. `publish_report` does NOT snapshot it, so it will expire and 404 in the frozen report. Check the URL form before relying on it; when it is a Meta CDN link the asset isn't yet freezable (re-sync/backfill the creative, or omit it). Either way, do NOT base64-inline the media. Use `image_url` / `video_url` as the durable source - the raw `creatives` table (via the `query` DSL) does not carry a public image/video URL at all. hook_type / format_type / intelligence are null until the Gemini tagger has analyzed the creative. For raw column-level access to the analysis across all creatives, the `query` DSL exposes the `creative_intelligence` table. Args: brand: Brand name or id to query. Required when you can reach more than one brand; optional (auto-selected) when you can reach exactly one. Call list_brands to see options. account: Optional Connected Account or list of them (each a display name, platform account id, Kreel id, or a saved Account Group name, which resolves to its member set; several values union) to narrow the creatives to. Omit it to read every account the brand holds; a brand with several accounts on one platform then gets an `accounts` manifest naming what was folded in. For Google, a named account overrides the credential's selected customer. An account the brand does not hold returns {"error": "unknown_account", "available_accounts": [...]}. platform: "meta", "tiktok" or "google". Google is metrics-only in v1; TikTok is ad-grain creative parity (see above). filters: Meta and TikTok: {"ad_id": "...", "campaign_id": "...", "adset_id": "...", "status": "ACTIVE"} plus the AI tag fields (hook_type, format_type, angle_category, offer_type, persona, talent_type, cta_type, visual_style, seasonality) and "custom:<key>" for the Brand's custom tag dimensions, each matched by exact value or "untagged" for creatives with no value. Meta also accepts configured naming-schema fields and naming_matched; TikTok also accepts adgroup_id. Unknown keys are rejected. Filters apply in every mode, including rollup (e.g. hook breakdown within a campaign). Google v1 rejects filters explicitly. group_by: Meta supports ["hook_type", "format_type", "asset", "angle_category", "offer_type", "persona", "talent_type", "cta_type", "visual_style", "seasonality"], "custom:<key>" and configured naming-schema fields. TikTok supports the same tag dimensions and "custom:<key>" with "media_type" in place of "asset". Dimensions can be combined per platform. Creatives with no value on a tag dimension form an "untagged" bucket so spend reconciles. When set, returns blended performance per group under `groups` instead of per-creative rows. Other keys are rejected. Google v1 rejects group_by. rank_by: Meta or TikTok rollup metric to rank by, e.g. "roas" or "cpa". Requires group_by; incomplete and undefined buckets are excluded. min_spend: Non-negative Base-currency spend floor for ranked groups. Requires group_by and rank_by; defaults to zero. date_range: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} OR {"days": N}. Default last 7 days ending yesterday. Scopes both delivery (inclusion) and the attached metrics. fields: one of "core" | "tags" | "all", escalating by real payload weight (default "core"): - "core" (default, lightweight): identity (id/name/adset_id/campaign_id/ status) + tag LABELS (hook_type/format_type, the v2 tags, custom_tags, lifecycle_stage) + numeric metrics + the fatigue block. NO Gemini prose, NO media URLs, NO copy. - "tags": "core" + the `intelligence` prose block (6 Gemini fields). - "all": "tags" + media URLs (image_url/video_url) + copy (headline/body/ call_to_action). ⚠️ Media and copy appear ONLY in "all". Google v1 supports only "core" and returns an explicit error for the Meta-specific "tags" and "all" tiers. limit: Max per-creative rows (default 200, hard ceiling 1000 — a token-safety backstop on the heavy fields="all" path). Caps ranked rollups; ignored for unranked rollups. The response also carries a `coverage` block over the returned creatives — {returned, tagged, untagged, with_media} — so you can tell when tag-based cuts are thin (the Gemini tagger may not have processed every creative yet). In per-creative mode `coverage.tagged` counts creatives with a hook/format tag OR Gemini intelligence (independently nullable), so it can overstate `hook_type`/`format_type` label coverage specifically. The counts cover the returned page (capped by `limit`), not the whole library. `truncated` is true when the page filled to `applied_limit` (more delivered creatives may exist below the spend-desc cut — widen `date_range` or raise `limit`). Truncation ties out: `total_creative_spend` is the spend of ALL delivered creatives matching the filter (no limit), and `omitted` = {count, spend} is what falls below the cut, so Σ(returned spend) + omitted.spend == total_creative_spend (both zero when not truncated). `total_creative_spend` reconciles the page to its own creative-delivered universe; it may still differ slightly from account-level Meta spend (ads with no creative record, deleted ads). Google responses use the same pagination reconciliation shape with `count`, `applied_limit`, `truncated`, `total_creative_cost`, and omitted count/cost. Their `coverage` block is {returned, total, truncated}; tag/media coverage does not apply to the metrics-only Google v1 surface.
get_creatives
Cross-brand performance summary for every brand you can reach, in one call. Built for the agency workflow — "what changed across my clients this week, who needs attention?" — without an agent making N sequential get_performance calls. There is no `brand` argument: like `list_brands`, this operates on your ENTIRE reach (`CallerReach.accessible_client_ids`), never a single selected brand. Rows are grouped under `workspaces`. A Supabase user who belongs to more than one workspace gets one group per workspace; an API key reaches exactly one workspace, so it always yields a single group. Each group carries `workspace_id`, `workspace_name`, `total_brands` (every brand you can reach in that workspace, before `limit` truncates the list), and `brands` (up to `limit`, SORTED WITHIN THIS GROUP by `sort_by` — never across groups). Each brand row: - `id`, `name`: pass `id` as the `brand` argument to get_performance, compare, or get_brand_context to drill into that brand. - `currency`: the currency the figures are reported in — the Shopify-reported account currency when available (the same one the revenue module resolves), falling back to the brand's configured base currency. There is no FX conversion — a raw `revenue`/`ad_spend` comparison across brands with different currencies is comparing different units. - `revenue`: refund-correct store-net revenue for `date_range` (the exact Period Revenue basis as get_performance's Shopify `revenue_kind= "store_net"` — reconciles digit-for-digit with get_performance for the same brand/window). `revenue_kind` is always `"store_net"` here; platform-attributed revenue appears only inside `platforms`. - `orders`, `aov`: the same refund-correct Shopify order count backing `revenue`, and net revenue / orders — `aov` is null when orders is 0. - `store_reason`: present when the brand has no Shopify store in scope (none connected, or one still backfilling this window); `revenue`, `orders`, `aov` and `mer` are then null, never 0, and `sparkline` is omitted. Say "no store connected" for those figures. - `ad_spend`: paid-channel (Meta, Google, TikTok) spend from `daily_channel_summary` for the window, each channel rounded to cents (email/organic carry no ad spend). - `mer`: store-net revenue / ad_spend (blended marketing efficiency, the same ratio get_performance's account-level summary reports) - null when ad_spend is 0, never a division error, and null with `store_reason` when the brand has no Shopify store connected (never 0); null without `store_reason` means no spend. - `new_customer_share`: new-customer net revenue / store-net `revenue` (0-1), Brand-new basis (a customer's first included order anywhere in the brand) - the same figure get_performance's account summary reports; omitted when revenue <= 0 (undefined, never 0). The response's `new_customers_basis` names the basis. - `cm1_pct`, `contribution_after_ads_pct`: profit ladder levels as 0-1 shares of net merchandise (ex tax/shipping, current unit cost), null unless the level is reported (below 80% cost coverage, no sales, or no ad account). Ratios only, so they compare across currencies. - `payment_fee_rate`: payment fees (actual Shopify Payments fees, else gateway rules) as a positive 0-1 share of net merchandise; null unless the ladder's payment_fees level is reported (80% fee coverage). - `repeat_rate_90d`: share of the brand's customers who placed a second order within 90 days of their first, over ALL retained history (not `date_range`) - the Repeat screen's default figure and the DSL `repurchase_metrics` overall `order_n=1` row. Customers whose first order is under 90 days old are excluded (right-censoring); `repeat_rate_90d_customers` is that denominator (omitted when the brand has no orders). `repeat_rate_90d` is null when no customer is eligible yet, never a measured 0. Observed, not predicted. - `champions_share`, `at_risk_share`: the latest weekly RFM run's Champions and At Risk customers as 0-1 shares of all scored customers (each person once across the brand's stores), whatever `date_range`. Omitted when RFM has never scored a customer, never a measured 0. Segment detail is the DSL `rfm_segment_summary` table. - `overdue_share`: the same run's overdue customers (days since their last order past the p75 of time between orders for their first product, else the store; not lapsed) over all scored customers, 0-1. Omitted likewise; detail is the DSL `at_risk_summary` table. - `cost_coverage_revenue_90d`: share (0-1) of trailing-90-day sold revenue whose variants have a usable unit cost, whatever `date_range`; null without Shopify sales. Below 0.8 margin is withheld - the brand needs costs loaded (set_product_costs, or /product-costs). - `missing_core_flows`: how many core lifecycle flow types (welcome, abandoned_checkout, browse_abandonment, post_purchase, winback) have no live Klaviyo flow, whatever `date_range`. Omitted without a Klaviyo account. Detail is the DSL `email_flow_types` table. - `platforms` (present only when the brand has ad/email rows in the window): per-platform "most important metrics" rollup. `meta` and `google` share the same base metric set - `spend`, `attributed_revenue`, `roas`, `cpm`, `conversions` (fractional, KRE-200) - so blended `cpa` (spend / conversions) is derivable per platform; `meta` additionally carries `ctr` (0-1 fraction) and `cpa`, `google` additionally carries `cpc`. `email` carries `attributed_revenue`, `revenue_share` (email attributed revenue / store-net revenue), `open_rate`, `click_rate` (unique engagement over delivered, 0-1 fractions). All platform revenue is PLATFORM-attributed - a different lens than the store-net `revenue`/`mer` above. Every division is null when its denominator is 0, and a platform with no rows in the window is omitted, never zeroed - a structural gap must not read as a measured 0. - `connected_platforms`: count of the brand's currently active platform connections. - `data_through`: the OLDEST per-platform freshness date among the brand's connected platforms (the same freshness get_brand_context reports per connection) - so one stale connection makes the whole brand read as stale even if its other platforms are current. A connected platform with NO queryable data yet nulls `data_through` entirely (it never silently drops out of the minimum); when that happens, `platforms_missing_data` (present only when > 0) counts the connected platforms with no data. `data_through` is also null when the brand has no connections at all (`connected_platforms` is 0 and `platforms_missing_data` is absent). Call `get_brand_context` on a brand flagged stale here before trusting its numbers. - `source_status`: the brand's sync-health state, its worst per-source freshness verdict (`reauth` > `failed` > `stale` > `backfilling` > `fresh`; `backfilling` only when every source is otherwise fresh), the same state `list_brands` gives as `health.state`; absent when the brand has no connected or retained source. - `comparison` (present only when `compare_to_previous=True`): the ADR-0010 comparison block versus the equal-length immediately preceding window - `mode`, `start_date`, `end_date`, plus `metrics` (the SAME shape as the row's own figures, computed over the comparison window). Coverage is per SECTION: a section whose source history does not span the window (base Shopify figures / blended ads / each platform) is absent from `metrics` and named in `coverage_gaps` with a machine-readable reason - never a delta against partial sums. `reason` replaces `metrics` only when no section at all is comparable. - `vs_previous` (present only when `compare_to_previous=True` AND the comparison has `metrics`): the KRE-174 back-compat delta trio `revenue_delta_pct`, `spend_delta_pct`, `mer_delta_pct`, derived from `comparison.metrics`. Each is null when its prior-period denominator is 0 or undefined, per the shared KRE-128 delta convention. - `pace` (present only when the brand has a budget plan value or monthly target for the month): Target pace, ALWAYS month to date on the brand's reporting calendar through yesterday - independent of `date_range`. Straight-line method: expected to date = monthly target x days elapsed / days in month. Carries `month`, `as_of`, `days_elapsed`, `days_in_month`, `days_left`, `currency`, a cited `summary` list, and one entry per set target: `revenue` and `new_customers` (`pace_pct`, `gap`, `projected`; status behind <100 / on_pace <105 / ahead), `spend_budget` (neutral: `over` above 105% of pace, else `within`), `mer` (`attainment_pct`, met/ missed) and `new_customer_cac` (`over_pct`, met when at or under target). Thresholds apply to the percentage as displayed (whole percent, half-up), so 99.6% of pace is on_pace. Spend budget, MER and CAC are `not_scored` with a `reason` while a paid source is failed, stale or backfilling, or when the denominator is zero - never scored on partial data; revenue, new customers, MER and CAC are `not_scored` (`no_store_source`, `store_source_backfilling`) while no Shopify store is in scope, never behind against a 0. Each metric's `source` is `plan` (the active budget plan's line for the month) or `targets`. Same object as get_brand_context's `pace`. - `findings`: `{critical, warn}` counts of the brand's findings for the window (every get_findings kind, every account, baseline the 14 days before the window). Counts only - call get_findings with the brand for the cited detail. Null when unknown (no connections, or a detector failed for that brand), never a reassuring 0. - `decisions`: decision queue counts only - `open_proposals` awaiting approval, `reviews_due` (executed decisions past their review date, verdict not yet computed) and `verdicts_landed` in the window. Absent when all three are zero. Call get_decisions with the brand for detail. - `attention_tier` and `attention_reasons` (every row, whatever `sort_by`): the server-side "needs attention" rank, 1 = worst: 1 `reauth` (a source needs re-authorization or an account selected), 2 `failed` (latest sync failed/partial, or a token error), 3 `stale` (past its SLA or never synced; backfilling is not a problem), 4 `missing_data` (a connected platform has no data yet, or no store source is connected), 5 `off_target` (a SCORED `pace` metric is behind, over budget or missed - `not_scored` and brands without a plan or targets never count, so a brand with no store never raises it from its store targets; a plan-paced metric cites `budget_plan`), 6 `critical_findings`, 7 `decline` (store-net revenue or MER down 10%+ vs the comparison window, only where that section is covered; skipped when `compare_to_previous=False`), 8 `attention_minor` (warn findings, decisions to approve, reviews due), 9 nothing to act on. Each reason carries `code`, `tier`, `source` (`sync_health`, `freshness`, `targets`, `findings`, `comparison`, `decisions`), a `message` naming the source and time on the brand's calendar ("Meta sync failed 26 Sep 06:10"), and `platform`/`at`/ `metric`/`delta` where they apply. `attention_incomplete: true` means an input failed this call, so a reason may be missing - never read it as all-clear. Finding detail stays per brand: call get_findings. - `sparkline`: 13 complete Monday-Sunday weeks of refund-correct store-net revenue ending by yesterday on the brand's calendar, independent of `date_range`: `start` (first Monday), `end` (last Sunday) and `revenue`, one value per week, oldest first. - `report_cadence`, `next_report_due`, `report_due_soon`: the brand's report schedule (weekly or monthly; set on the Reports settings page), the next due date on its calendar (today included) and whether it falls within 7 days. Without a schedule, `last_published_at` is when its latest active report was published (absent if never). A schedule only drives reminders; nothing is published automatically. - `pending_report_approvals`: report drafts waiting for an owner/admin to approve them in the dashboard (Reports > Drafts) - open publish_report decisions plus held scheduled report runs; absent when none. Call get_decisions with the brand for the decision detail. - `disconnected_sources`: removed connections whose retained data still feeds the row (`platform`, `label`, `data_through`) - a note, never an attention reason. - `unavailable` (only when a block failed this call): block name (`findings`, `profit_ladder`, `cost_coverage`, `rfm_segments`, `freshness`, `pace`, `decisions`, `sparkline`, `reports`) -> `computation_failed`. That block's fields are null; the rest of the row is valid and the next call retries it. The response carries `source_summary`: how many returned brands are `fresh`, `backfilling`, `stale`, `failed` or `reauth` by their `source_status`; and `attention_summary`: how many returned brands have an `off_target`, `stale` or `reauth` reason, and `reports_due` within 7 days - counts only, never money. A workspace group also carries an optional `subtotal` (currency, revenue, ad_spend, orders, mer; its `revenue`, `orders` and `mer` cover only the brands with a Shopify store - null when none has one - while `ad_spend` covers every brand, and `store_reason` says when a brand was left out) ONLY when every brand reachable in that workspace shares BOTH the same base currency AND the same resolved revenue policy (Revenue Basis - tax/shipping inclusion - and excluded sales channels); otherwise it is omitted, never approximated - two same-currency brands whose revenue is computed under different definitions are just as non-summable as two currencies. There is deliberately NO cross-brand total anywhere else in the response — brands can report in different base currencies, timezones, and revenue policies, so summing them would misstate the portfolio. `notices` always states this explicitly. Args: date_range: Rolling window as a day-count string, e.g. "7d" (default), "30d", "90d" — the same convention as the `kreel` CLI's `--period`. Ends yesterday, like every rolling window in Kreel. sort_by: "attention" (default) | "revenue" | "spend" | "mer" | "new_customer_share" | "overdue_share" | "revenue_delta". "attention" ranks each group by `attention_tier` (worst first), then more reasons, then the worst decline, then name - deterministic, and the same order the dashboard's Portfolio shows. "revenue", "spend", "mer", "new_customer_share" and "overdue_share" sort each workspace group's brands DESCENDING (largest first). "revenue_delta" sorts ASCENDING (biggest decline first) so the brands needing attention surface at the top of their group; it requires `compare_to_previous=True` and is rejected otherwise. A brand with an undefined value for the chosen key (e.g. `mer` when `ad_spend` is 0, `revenue` for a brand with a `store_reason`, or a "new this period" `revenue_delta`) always sorts last within its group. limit: Max brands returned PER WORKSPACE GROUP (default 25). Capped at 50 regardless of the value passed - this tool runs several queries per brand, so a very large portfolio is capped for latency. Brands beyond the limit are dropped from `brands` but still counted in that group's `total_brands`. compare_to_previous: When True (the default - the OPPOSITE default of get_performance/compare), attach each row's `vs_previous` block. Set False for a faster call when you don't need period-over-period deltas; the attention rank then skips its `decline` tier rather than erroring. Results reflect data that has already been synced/materialized - a brand's `data_through` (and `connected_platforms`) tell you how current its figures are; call `get_brand_context` on any brand you plan to act on before trusting its numbers.
get_portfolio
Normalized performance query across connected platforms. Dimension vocabulary (ADR-0024): account rows use `channel` with values meta_ads, google_ads, tiktok_ads, klaviyo_email, klaviyo_sms, shopify_store. Unified ad campaign rows use `platform` with meta/google/tiktok. The `platform` argument selects an integration (also shopify/klaviyo/all); it is not a channel value or a Connected Account id. Shopify sales_channels are separate order sources. Args: brand: Brand name or id to query. Required when you can reach more than one brand; optional (auto-selected) when you can reach exactly one. Call list_brands to see options. account: Optional Connected Account, or list of them, to narrow the read to — each a display name, platform account id, Kreel id, or a saved Account Group name (a Brand-level named set of accounts; see `account_groups` in get_brand_context), which resolves to its member set; several values union into one set. Omit it (the default) to read the blend of every account the brand holds, which is what a brand with one account per platform has always got. A brand with several accounts on one platform gets an `accounts` manifest naming what was folded in, and any read narrowed with this argument carries the same manifest; when the selection is exactly one group, the manifest names it as `group`. An empty group reads as zero rows with `count: 0` — the group exists but has no readable members. Naming an account the brand does not hold returns {"error": "unknown_account", "available_accounts": [...]} (plus `available_groups` when the brand has groups) rather than an empty result that looks like "no data". platform: "all" | "meta" | "google" | "tiktok" | "klaviyo" | "shopify" | "ga4". "ga4" reads Google Analytics 4 website behavior for the brand's one selected property and web stream, a measurement source kept apart from Shopify commerce and ad-platform attribution. Its entity types: "account"/"website" (sessions, engaged_sessions, engagement_rate, purchase_sessions, purchase_session_rate, ecommerce_purchases, plus ga4_purchase_revenue in the property currency, never to be added to Shopify or ad revenue), "traffic_source" (session source / medium), "channel_group" (GA4 default channel group, paid and unpaid), "campaign" (GA4 session campaign id/name), "landing_page", "device", and "shopping_activity" (observed view_item / add_to_cart / begin_checkout / purchase counts - not a funnel). GA4 filters: channel_group, source_medium, device_category, landing_page (landing pages), campaign_id / campaign_name (traffic); an incompatible filter returns {"error": "incompatible_filter", "compatible_filters": [...]}. filters may also carry min_sessions (int) and, for landing_page, group_by ("device_category" | "channel_group" | "source_medium") to compare pages within one device or source. purchase_session_rate is GA4 sessionKeyEventRate:purchase recomputed from summed purchase sessions / sessions; null means the property has no purchase key event (see meta.warnings), not zero. Every GA4 result carries `meta`: status (ok | empty | partial | syncing | no_coverage | needs_selection | not_connected | permission_lost), the property / stream, exact windows, property vs brand time zone, currency, last_synced_at, coverage, quality (thresholding, (other) rows, sampling, provisional days) and warnings - disclose them. Rows are sessions-descending; `total_rows`/`truncated` say what `limit` cut. sales_channels does not apply to GA4. entity_type: "account" | "campaign" | "adset" | "ad" | "flow" | "email_campaign" | "search_term" | "keyword" | "product" | "product_group" | "asset_group" | "country". "adset"/"ad" support Meta Ads, Google Ads and TikTok Ads (platform="google" reads AdSetPerformance/GoogleAdPerformanceDaily; for Google and TikTok, "adset" is an ad-group alias — ad-group discovery is via get_performance(entity_type="adset", platform="google"|"tiktok"), or get_entities(platform="tiktok", entity_type="adset")). For "adset"/"ad", `platform` must be "meta", "google" or "tiktok"; platform="all" returns Meta ads only (Google and TikTok ads are NOT merged in) - query each platform separately to get its rows. Campaign rows carry `objective` — the raw platform objective / campaign type (Meta OUTCOME_SALES/OUTCOME_TRAFFIC/..., Google SEARCH/SHOPPING/ PERFORMANCE_MAX/..., TikTok WEB_CONVERSIONS/PRODUCT_SALES/SHOP/...) — so non-sales campaigns (Sampling/Awareness) read in context; it is omitted when unknown. TikTok ad/adset rows report `purchases` = TikTok website purchases (complete_payment) and `revenue` = their TikTok-attributed value, plus `cta_conversions` / `vta_conversions` — optimization-goal conversions by click-through / view-through attribution that are NOT purchases and are omitted when TikTok reported them unavailable. Every TikTok row carries an `attribution` block (click / view / engaged_view windows and event_count) exactly as TikTok reports it for the ad group; null means not reported, never inferred. The response's `semantics` block restates these definitions. `conversions` on TikTok campaign rows and the `tiktok_ads` account row are the same website purchases. "search_term" and "keyword" are Google Ads only; TikTok has no equivalent, and TikTok creative assets are available through get_creatives(platform="tiktok"). "product" returns Shopify product sales with product title, units, and refund-correct net merchandise revenue in one row. It honors the brand's Revenue Basis and sales-channel policy. Shipping is excluded because it cannot be allocated defensibly to products. Products that were sold but later deleted from the catalog use "Deleted product" as the title so their sales and refunds remain visible. "country" returns Shopify store-net revenue, orders, average_order_value and share_pct per shipping country (ISO-3166 alpha-2; "ZZ" = no usable address), ranked by revenue. It follows the Revenue Basis and sales-channel policy with cross-period refunds netted per country, so the rows sum to the account-level store net (`totals`); prefer it over the country_daily_sales query table when reconciling. "product_group" returns Shopping product performance (Google Ads only). "asset_group" returns PMax asset-group performance (Google Ads only); note that Google does not expose per-asset metrics — labels only, so metrics here are group-level aggregates. Account rows (entity_type="account") carry `revenue_kind`: "store_net" for the Shopify channel (refund-corrected NET revenue, not the stored gross) and "platform_attributed" for every ad/email channel. Attribution models and click/view windows are provider-specific, not necessarily last-click and not directly comparable across platforms. Do NOT sum `revenue` across channels - the bases differ (store cash-flow vs overlapping platform attribution). Shopify account rows also expose `gross_revenue` and `refund_adjustment`. When platform="all" and entity_type="account", a `summary` block adds blended `mer` = store-net revenue / total paid ad spend (a top-line efficiency ratio, not an attribution model); its `total_ad_spend` covers the whole window regardless of `limit`. `mer` and `store_net_revenue` are null with a `reason` when the `account` scope holds no Shopify store (never 0); `mer` is null without a `reason` when there was no paid spend. The same block also adds `aov` = store-net revenue / orders, omitted (key absent, not null) when orders is 0 - matching the roas/cpa omission convention used elsewhere in this tool's account/campaign rows. There is no cross-platform ROAS: each platform's attributed revenue is its own overlapping claim and Kreel never sums it. For paid efficiency read `mer` plus each ad row's own `roas`, identified by `channel` and `revenue_kind: "platform_attributed"`. When the scope covers the store, `summary.profit` carries the margin ladder (ex tax/shipping, current unit cost): `cost_coverage`, `net_merchandise`, then `cm1`/ `cm1_pct` and `contribution_after_ads`/`_pct` down to the last reported level; `payment_fees` carries `rate` (share of net merchandise), `coverage` and gateway `sources` (actual / rule / missing) once the payment class is configured; `withheld` names levels that are not_configured or below 80% coverage. Absent when the window has no sales. When the scope covers the store, the summary also splits store net by customer type (`new_customers_basis` "brand-new": history across every store): `new_customer_revenue` (customers' first orders), `returning_revenue` (repeat orders, reactivated included), `new_customers` (distinct), `new_customer_share` = new_customer_revenue / store_net_revenue (0-1, null when store net <= 0) and `new_customer_mer` = new_customer_revenue / total_ad_spend (null with no spend). For the full five-segment split (unknown history, unidentified) and daily series query the `customer_mix_daily` table. The summary also carries `targets` (only the Brand's set monthly targets: monthly_revenue_target, monthly_spend_budget, target_mer, monthly_new_customers_target, target_new_customer_cac; `{}` when none) and `pace`: month-to-date straight-line pace (the `get_brand_context` pace object: month, as_of, days_left and one block per paced metric with `status`, `summary` and `source`), or null with neither a target nor a budget plan value for the month. When the active budget plan covers the month, a metric with a plan value paces against the plan (`source: "plan"`, `pace.plan_version`), so its `pace` target can differ from `targets`, which lists the Profile targets only. Pace is always Brand-wide month to date on the Brand calendar, whatever `date_range` or account filter this call uses. `roas` convention (account/campaign/adset/ad rows): the key is present only when spend > 0 - a real 0.0 when spend > 0 and revenue = 0, and absent (not null) when spend = 0, since ROAS is undefined with no spend. `cpa` follows the same spend > 0 (and, where applicable, count > 0) presence rule. This omission shape is deliberate and by design, not a gap: do not treat a missing `roas`/`cpa` key as 0. date_range: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} OR {"days": 7}. Default is the last 7 days ending yesterday. filters: Optional entity filters. For Meta and TikTok ad/adset performance: {"campaign_id": "...", "campaign_name": "...", "adset_id": "...", "ad_id": "...", "name_contains": "..."} (TikTok also accepts "adgroup_id" as a synonym of "adset_id"). Klaviyo email_campaign/flow queries accept {"send_channel": "email" | "sms"}. Other filters on other entity types are ignored. `name_contains` is a case-insensitive substring match against the campaign, adset, and ad names (a row matches on any of the three). metrics: Optional output column narrowing. When given, each row keeps its identity/dimension keys (id, name, channel, campaign_id, status, ...) plus only the listed metrics (e.g. ["spend", "roas"]); requested names absent from every row (unknown, or not produced by this entity_type) are echoed back in `ignored_metrics`. An empty list narrows to identity-only; omit the arg entirely to return all standard metrics. limit: Max rows to return (default 50). Rows are the top `limit` entities ranked by spend (or by revenue/cost, depending on entity_type), descending; lower-ranked rows are dropped silently with no more-rows indicator - raise `limit` for completeness. compare_to_previous: When True, attach a per-row `vs_previous` block (and one on the MER `summary`) comparing each row to the equal-length prior window, and a top-level `comparison_period: {"start", "end"}` disclosing the resolved prior window's dates (same shape as `period`). Supported for entity_type "account", "campaign", and "product" only; ignored otherwise (no `comparison_period` is attached). Product matching uses the current top rows' product ids, so a lower prior rank cannot erase its baseline. `vs_previous.previous` is the prior row's metrics (or null = "new this period"); `vs_previous.delta_pct` is (current − previous) / previous per metric (null when previous is 0). Off by default to keep responses compact. sales_channels: Optional Shopify `source_name` allowlist, for example ["web", "pos"]. This override applies to Shopify-derived revenue and orders on account and product queries where platform is "shopify" or "all". It wins over the brand exclusion policy. Ad-platform and Klaviyo values remain unfiltered, so blended MER uses filtered store revenue over full paid-ad spend. Values are normalized to lowercase; use "__unknown__" for orders without source_name. Unknown values return an error with the observed values instead of being ignored. Omit the argument to use the brand policy. An empty list is invalid. Also applies to entity_type "country". daily_series: When True (entity_type "product" or "country" only, and limit <= 5), each row carries `series`: one {"date", "revenue", "units"|"orders"} point per day of the window, in date order and zero-filled, summing to that row's revenue and units/orders (a refund lands on its own day, so days can be negative). A limit outside 1-5 returns {"error": "daily_series_limit", "max": 5}; other entity types return {"error": "daily_series_unsupported", ...}. view: Optional saved view or Ask snapshot id (list_views / get_view). It fills date_range, compare_to_previous (equal-length previous only), account and sales_channels where you left them at their default; explicit arguments win. The response echoes `view: {id, name, applied, not_applied}`. Notes: - Derived ratios (roas, cpa, ctr, cpc, cpm, conversion_rate, open_rate, click_rate, bounce_rate, unsubscribe_rate, spam_rate, revenue_per_recipient, revenue_per_delivered) are OMITTED from a row when their denominator is 0 (undefined), not returned as 0. - Email revenue_per_recipient / revenue_per_delivered are Klaviyo- attributed revenue ÷ recipients / delivered on the send-date basis. Each email row is one send channel (email or SMS); never average or sum them across rows or channels - recompute from revenue and recipients/delivered. - All rate metrics are 0-1 fractions (never 0-100 percentages), matching the query tool and REST. `conversion_rate` denominators differ by entity: email rows use conversions/delivered; ad/adset rows use conversions/clicks. - Flow rows omit `bounced`/`spam_complaints` (and their rates): Klaviyo flow-series reports do not measure them, so absence means "not tracked", not zero. Campaign rows report them as measured. - Meta ad/adset rows also carry mid-funnel counts `add_to_cart` / `initiate_checkout` (their `*_value` totals only when nonzero), `leads`/`lead_value` (only when nonzero) and, when the row is a video creative (per MetaAd metadata) or the window saw video delivery, the video funnel: `video_views` (Meta 30-second views, not 3-second) + `video_views_p25/p50/p75/p95/p100` percentile completions — a delivered video nobody watched shows measured zeros. The video keys are omitted entirely for static/unknown-media rows with no video delivery — a structural gap, not a measured 0. Adset rows can mix formats, so they use the delivery signal only. - `landing_page_views`, `video_3_sec_views` (hook-rate numerator), and `thruplays` appear only when EVERY row in the window carries them (synced after the KRE-279 extension) — a window crossing the rollout boundary omits them instead of publishing a partial total next to full-window spend. Re-sync history to fill. Absence means "not fully synced for this window", never a measured 0. `video_avg_watch_time_sec` is an unweighted mean of per-day averages over the reported days. - `cpa` denominator varies by level: Shopify account rows use store orders; attributed-channel account rows and campaign/adset/ad rows use platform conversions/purchases - so `cpa` is not directly comparable across channels or levels. - All monetary values are in the brand's single base currency; there is no per-row currency field. - Blended figures are summed from each account's own rows, so every derived ratio (roas, cpa, ctr, cpm, ...) is Σ numerator / Σ denominator across the accounts — never the average of per-account ratios. - Results reflect data that has already been synced/materialized. Call `get_brand_context` before this tool and disclose its readiness warnings before interpreting rows. Empty `rows` may mean a channel is not yet synced rather than genuinely zero activity.
get_performance
Read one decision (with its frozen payload) or a bounded page of summaries. `status_group`: `open` (proposed), `executed` (executed, verified, reviewed_no_verdict) or `closed` (rejected, withdrawn, expired). A rejected decision carries the human's `rejection_reason`; do not re-propose it unchanged. A proposal past its expiry reads as `expired`. Verdicts land within about 15 minutes of a decision's review date; until then an executed decision past its review date reads as `executed`.
get_decisions
Compare provider engagement, observed-attempt bounce rates and complaints. Defaults to 28 completed days versus the preceding 28. Rates and deltas are fractions. Includes daily counts, period bounce mix, stored message/flow evidence, campaign send-date trends, collection coverage and freshness. Unknown ownership is retained. Daily unique opens are not period-unique recipients; incomplete or invalid rates are null with a reason. Account-total fallback measurements are excluded from this report. Read them through query on klaviyo_deliverability_daily with grain=account; they have no provider attribution. Collection coverage explains when fallback occurred. Account selectors use the same tenant-scoped account names/IDs/groups as get_performance.
get_deliverability
Read a saved experiment or a bounded page; reads never compute verdicts.
get_experiments
One normalized, cited list of what needs attention for a brand. Findings come from Kreel's existing detectors, never new logic: `anomaly` (channel/campaign spend, revenue, orders, roas, cpa, ctr, cpm - the detect_anomalies alerts), `deliverability` (Klaviyo campaign bounce/spam rates), `stockout_risk` and `dead_stock` (inventory cover), `high_return_rate` (product/variant refund activity), `creative_fatigue` (Meta ads: CPC rise with hook or ROAS decline, always `warn`) and `pacing` (an ad platform whose projected month-end spend, linear from the trailing 7-day average, deviates from its budget plan line beyond the Brand's tolerance, always `warn`; evidence carries the advisory daily run-rate change - see get_budget_pacing). `kinds` in the response lists every kind with its owning detector and severity thresholds; `sync_stale`, `proposal_pending`, `review_due` and `verdict_landed` are reserved and have no detector yet. Each finding carries `fingerprint` (stable across reads and days for the same brand, kind, subject, metric, direction and severity - use it to dedup), `severity` (critical | warn), `subject` (exactly one channel, campaign, ad, product or variant - never a cross-platform blend), `window`/`baseline_window` (the detector's own windows: fatigue always compares seven days with the prior seven, inventory uses trailing 28-day velocity, returns compare with the store in the same window), `revenue_kind` (store_net or platform_attributed; never add the two), `evidence` (the detector's payload verbatim), `source_ids` (each a key in `sources`) and `deep_link` (the dashboard page with brand and dates applied). Inventory and returns metadata appear once in `metadata`. Google conversions stay fractional. `magnitude` is the finding's size as a multiple of its kind's warn threshold (1 = at warn; null when evidence lacks it). `page` returns exactly the list a dashboard screen's "What changed" rail shows: `performance` (every kind), `ads` (Meta/Google/TikTok anomalies and pacing), `creative` (fatigue), `email` (Klaviyo anomalies and deliverability) or `store` (stockout, dead stock, returns). A page list is ranked by severity, then magnitude, then registry order. Args: window: Recent period, default {"days": 7}; or {"start", "end"} ISO dates. baseline_window: Comparison period for anomaly and deliverability findings. Defaults to the 14 days immediately before ``window``. kinds: Optional subset of the active kinds. An empty list, an unknown kind or a reserved kind returns a structured error. severity: Optional subset of "critical"/"warn". An empty list or an unknown value returns a structured error. account: Optional Connected Account(s) or Account Group name, as in detect_anomalies; an account the brand does not hold returns {"error": "unknown_account", "available_accounts": [...]}. page: Optional dashboard page (see above). Unknown pages, and `kinds` outside the page's kinds, return a structured error. platform: Optional subject platform (meta, google, tiktok, klaviyo, shopify). Applied before `counts` and `limit`. brand: Brand name or id. Required when more than one brand is reachable. limit: Max findings returned (default 50, capped at 200), most severe first. `counts` covers every match and `truncated` says whether more matched.
get_findings
Every cited number a templated report needs, assembled server-side. The report template gallery's data half: each template is also a prompt of the same name. You write the prose; the scaffold carries the figures, so never recompute or estimate one. Publish the result with `publish_report` built on the `kreel://report-kit` resource. Templates and their `window`: - board_monthly_summary: YYYY-MM; vs the previous month and the same month last year. - qbr: YYYY-Qn; vs the previous quarter and the same quarter last year. - client_weekly_report: the week's last day as YYYY-MM-DD; vs the previous 7 days. - promo_recap: START..END (required); vs `baseline` (START..END, must not overlap) or, by default, the equal-length window just before it. - creative_review: START..END; vs the previous equal window. - portfolio_rollup: YYYY-MM; every Brand you can reach (agency internal, `brand` is ignored), each vs its equal-length previous period. Omit `window` (or pass 'latest') for the last complete period. Windows are inclusive days on the Brand reporting calendar and must end before today. The response has `window`, `comparisons` (a comparison whose history the Brand's sources do not cover comes back with status 'omitted' and its reason), `sections` and `sources`. Each section has `status` ('ok' or 'omitted' with `omitted_reason`, e.g. a platform that is not connected: render the reason, never a zero), `values` (metrics with `comparisons` deltas and, where the Brand has one, a `target` chip: the active budget plan's line for each month, else the saved Monthly target, the same target Target pace and plan_vs_actual use), `rows` (tables), `notes`, `truncated` (rows cut by the section's cap) and `source_ids`. Every number sits under a `source_id` that resolves in `sources`, which is also the report's definitions footer. Store revenue is refund-correct net. A section with `do_not_add` holds platform-attributed figures: never add them across channels or to store revenue. Contribution margin appears only with its cost coverage and the current-cost label. Google conversions are fractional; email rates are 0-1. Args: template: One of the template names above. window: The template's window (see above); omit for the latest. baseline: promo_recap only: START..END, or omit for the default. brand: Brand name or id. Required when more than one brand is reachable; ignored by portfolio_rollup.
get_report_scaffold
List the brands (clients) you can query. Use this first when you can reach more than one brand, then pass the chosen brand's name or id as the `brand` argument to other tools. If a brand's display name can't be resolved, `name` falls back to the id string. Each brand carries `health`, the same sync-health verdict the dashboard's status pill shows: `state` is the worst of its connected sources (`reauth` > `failed` > `stale` > `backfilling` > `fresh`, or `none` with nothing connected; a `disconnected` source's retained data is listed but never rolled up), and `sources` lists each source's `status`, `reason`, `data_through`, `last_success_at` and `backfilling` progress. Check it before trusting a brand's recent numbers: `reauth` and `failed` mean data stops at that source's `data_through`.
list_brands
List the custom dashboards you can read on a brand. You see the brand's dashboards, the workspace's templates, and your own private ones. Each entry names its visibility, creator, range, tile count and whether you can edit it. Read one with its tiles' numbers via get_dashboard; build one with create_dashboard. Args: brand: Brand name or id. Required when you can reach more than one brand.
list_dashboards
Structural fetch of platform entities. Args: brand: Brand name or id to query. Required when you can reach more than one brand; optional (auto-selected) when you can reach exactly one. Call list_brands to see options. account: Optional Connected Account or list of them (each a display name, platform account id, Kreel id, or a saved Account Group name, which resolves to its member set; several values union) to narrow the entities to. Omit it to list the entities of every account the brand holds on this platform; a brand with several accounts on one platform then gets an `accounts` manifest naming them. An account the brand does not hold returns {"error": "unknown_account", "available_accounts": [...]}. platform: "meta" | "google" | "tiktok" | "klaviyo" | "shopify". entity_type: platform-specific (see supported_combinations in errors). Beyond id/platform_id/name/status, entities carry synced structural fields (omitted when unset): - meta campaign: objective, effective_status, daily_budget, lifetime_budget, budget_remaining, buying_type - meta adset: campaign_id, effective_status, daily_budget, lifetime_budget, bid_amount, bid_strategy, optimization_goal, start_time, end_time - meta ad: campaign_id, adset_id, effective_status - google campaign: channel_type, channel_sub_type, serving_status, is_removed, bidding_strategy_type, budget_period (DAILY | CUSTOM_PERIOD), budget_delivery_method, budget_explicitly_shared (budget amounts and pacing: get_budget_pacing) - tiktok campaign: objective_type (raw TikTok objective, e.g. WEB_CONVERSIONS / PRODUCT_SALES / SHOP), campaign_type, operation_status, secondary_status, is_deleted, budget, budget_mode, created_time, modified_time - tiktok adset (= TikTok ad group): campaign_id, operation_status, secondary_status, is_deleted, optimization_goal, optimization_event, promotion_type, budget, budget_mode, schedule_type, schedule_start_time, schedule_end_time, and the attribution windows TikTok locked for the ad group (click_attribution_window, view_attribution_window, engaged_view_attribution_window, attribution_event_count) — absent when TikTok did not report them, never inferred - tiktok ad: campaign_id, adgroup_id, operation_status, secondary_status, is_deleted, ad_format, video_id (fetchable creative asset; absent for Spark Ads without one), tiktok_item_id, identity_type, call_to_action, landing_page_url - klaviyo campaign/flow: tags as ``[{id, name}]`` - klaviyo flow: last_sent_date, the latest day it delivered a message in retained history (null when none is retained). `status` is the current state only, so a draft flow can still show sends in metrics_7d TikTok `status` is the effective delivery state (secondary_status, e.g. CAMPAIGN_STATUS_ENABLE / CAMPAIGN_STATUS_DELETE) falling back to the advertiser's operation_status; deleted entities are kept so their history stays readable, filter {"is_deleted": false} to hide them. Budget/bid amounts are in the ad account's currency. Google campaign budgets and bid strategies are NOT synced, so they are not returned. Meta budgets: a campaign missing daily_budget/lifetime_budget while its ad sets carry them means Ad Set Budget Optimization (ABO) - read the budget from the ad sets. A campaign carrying a budget while its ad sets don't means Campaign Budget Optimization (CBO) - read it from the campaign. If neither level carries a budget, treat it as unavailable (e.g. an ended or stale budget object) rather than a sync gap; this is a heuristic, not a guaranteed classification. Google serving_status is raw Google API passthrough: its enum (SERVING/NONE/ENDED/PENDING/SUSPENDED/UNKNOWN) has no REMOVED member, because serving eligibility and campaign lifecycle are different Google dimensions. `status`/`is_removed` are the authoritative lifecycle fields. To avoid returning a value that contradicts them, `serving_status` is omitted entirely whenever `is_removed` is true. filters: e.g. {"status": "ACTIVE"}. Applied as equality on the matching column. Keys are validated against that (platform, entity_type)'s real table columns; a key outside that set returns {"error": ..., "valid_filters": [...]} instead of an unfiltered or partially-filtered result. include: ["metrics_7d"] attaches a 7-day perf rollup per paid campaign or Klaviyo campaign/flow. `metrics_7d` always covers the fixed 7 days ending yesterday. It is zero-filled when an entity has no data in that window. Email rollup rates (`unique_open_rate_7d`/`unique_click_rate_7d`) are canonical 0-1 fractions, matching get_performance. ["content"] on a Klaviyo flow attaches `messages`: each flow message that has sent and been captured, with the same fields as below plus `as_of` - it is the message's current version in Klaviyo, re-checked about daily, so earlier sends may have used an earlier version (`removed_in_klaviyo` marks one Klaviyo no longer has; the content is its last captured version). ["content"] on a Klaviyo campaign attaches what it sent: `content_status` (captured | pending = sent, captured on the next Klaviyo sync | not_sent | unavailable = Klaviyo returned no messages) and, when captured, `messages` with one entry per message (an A/B test has one per variation, named by `label`): subject, preview_text, from_email, from_label and the email's plain `text` (SMS: the message body), capped at 2000 characters with `text_truncated` when cut. Text is the template as a generic recipient sees it: personalization shows its default or a `[name]` placeholder, conditional blocks keep every branch, and content that lives only in images is not in it. limit: Max rows (default 50). The response's `total` is the full pre-limit match count (raise `limit` if `total` exceeds `count` and you need completeness); `count` is just the number returned. Ordering: for platform meta/google/tiktok with entity_type="campaign", rows are sorted by 7-day spend descending (same window as `metrics_7d`), ties broken by name ascending, then platform_id ascending as the final tie-breaker. Every other (platform, entity_type) - meta/google/tiktok adsets and ads, all Klaviyo entity types, and Shopify products - has no campaign-grain spend concept, so those sort by name ascending, falling back to platform_id ascending when name is null or tied.
get_entities
List the dashboard saved views you can read on a brand. A saved view is a named page state from the Kreel dashboard: date range, Compare To, account and sales-channel filters, and the page's own filters (Creative: platform and filter bar; Email: tab and filters). You see brand views, the workspace's templates, and your own private views. Each entry names its page, visibility, creator and whether it is the brand default or your default. Read one with get_view, or pass its id as `view` to get_performance or query. Ask snapshots (from "Ask about this view") are not listed; their id arrives with the question. Args: brand: Brand name or id. Required when you can reach more than one brand. page: "creative" | "email" to narrow the list; omit for every page.
list_views
List the Brand's scheduled agents and the templates a job can run. Each job shows its template, schedule (Brand-local), output (a hosted report or a Slack/email message), destinations, approval mode (auto, or hold drafts for an admin), status, next run and last run. Run history is on each run's id: call ``get_agent_run``.
list_agent_jobs
Queue a fully materialized action for human approval. Nothing executes now. This tool only stores a pending proposal inside Kreel: nothing is published, sent or changed anywhere else. An owner/admin reviews the frozen proposal in the Kreel dashboard Decisions queue; approval executes exactly this payload server-side, unchanged. Proposals are immutable: to change one, propose again with `supersedes=<decision id>`, which withdraws the old proposal. `payload` is one of (keyed by `action_type`): - `publish_report` {title, html, supersedes?, correction_note?}: the complete report HTML. It is frozen now as a draft (report kit, assets, branding and data-trust footer, exactly as `publish_report` would store it; the returned decision carries its `report_id`). Only a human approving it in the dashboard publishes it, unchanged; no public URL serves it before then. A live report with the same title is refused unless `supersedes` names it; `correction_note` (one line on what changed) is required with `supersedes`. - `record_learning` {topic, content, evidence?} - `update_section` {key, content_md}: key is positioning, icp, competitive_landscape or brand_voice. `rationale` (<= 4000 chars) argues why; `evidence` cites up to 20 source references (e.g. `source_id`s from the growth brief or get_experiments). `verification` is optional: the experiment shape from record_experiment (hypothesis, subjects, expectation, baseline, controls) without dates. On approval an experiment opens whose observation window starts the Brand calendar day after execution and lasts `review_window_days` (1-90, default 14); its verdict is computed on the first read after that. Omit it when the action has no falsifiable metric outcome (a report usually has none); the decision then closes as `reviewed_no_verdict`. Proposals expire after `expires_in_days` (1-30, default 14). A brand can hold at most 50 open proposals. Call get_decisions (or read get_brand_context `decisions`) first so you do not re-propose something already pending or rejected.
propose_decision
Publish an HTML report and get back a shareable URL, or save it as a draft for approval. **Drafts and approval.** With ``draft=true``, or whenever the brand requires approval for agent-published reports (on by default for Agency workspaces), nothing is published: the complete report is frozen as a draft and queued as a ``publish_report`` decision for an owner/admin to review in the Kreel dashboard (Reports > Drafts). The call then returns ``status="draft"``, the ``report_id`` and ``decision_id``, ``url=null`` and ``viewable=false``; no public URL serves a draft. Approval publishes exactly the frozen draft. Follow up with ``get_decisions(decision_id=...)``: a rejected draft carries the reviewer's ``rejection_reason`` - address it before proposing again. ``rationale`` (why this report, <= 4000 chars) and ``evidence`` (up to 20 source references) are shown to the reviewer. **To correct or retry a previous publish, never call this twice with the same title.** If a LIVE report (not yet superseded) with that exact title already exists in this brand, the call is refused - no new report is created - and the refusal names the existing report's ``existing_slug``/``existing_url``. Get human approval for the replacement, then re-call ``publish_report`` with ``supersedes=<existing_slug>``. To publish an unrelated report that happens to share a title, change the title instead. ``supersedes`` accepts any report previously published in this brand, including one that has already been superseded itself - it always links at the current end of that report's correction chain, so old shared links keep resolving forward to the newest version. Reports are otherwise frozen: there is no edit call, and deletion is a permanent, human-only action in the dashboard - this tool can only create. The report is hosted at ``{handle}.reports.kreel.ai/{slug}`` and is **frozen** (immutable) once published. Access is gated by the Brand's report password, which an owner/admin sets in the dashboard ``/reports`` section; until a report handle is claimed AND a password is set, the report is not viewable. Serving also requires the owning Workspace to be in good billing standing - a report goes dark (stops serving) if the workspace subscription is cancelled or unpaid. **Images are snapshotted at publish - short-lived signing does not matter.** Any ``<img>`` (or other URL) pointing at a Kreel-hosted creative asset - the Supabase signed URLs of the form ``…/storage/v1/object/sign/creative-assets/…`` returned as ``image_url`` / ``video_url`` by ``get_creatives(fields="all")`` - is **copied into this report's own storage at publish and inlined into the frozen document** (oversized assets fall back to a durable ``/{slug}/asset/…`` on the report origin), served permanently. So the 1-hour expiry on those signed URLs is irrelevant: it only has to be valid at the moment you call ``publish_report``. You do NOT need to strip photos to keep a frozen report from breaking - reference the signed URL as-is. What is NOT snapshotted, and WILL 404 / rot in the frozen report: relative or local paths (e.g. ``assets/foo.jpg`` - they resolve against your local machine, not the report origin) and non-Kreel URLs (e.g. Meta CDN links, which expire on Meta's schedule). Note that ``get_creatives(fields="all")`` is not a guarantee of the safe form: ``image_url`` / ``video_url`` return the Kreel signed URL only when the asset has been stored, and otherwise fall back to a Meta CDN URL that will rot. So reference a creative URL as-is only after confirming it is the signed form (``…/storage/v1/object/sign/creative-assets/…``); if it is a Meta CDN link the asset isn't yet freezable - re-sync/backfill the creative, or omit it. Kreel automatically adds a frozen data-trust section to the stored HTML. It identifies connected sources, their last successful sync and data-through dates, the definitions of common report metrics, and the attribution caveat that platform-reported revenue overlaps and must not be added across channels. This section is derived server-side and does not need to be included in ``html``. **Margin levels need their basis.** Whenever the report quotes a margin level (net merchandise, product margin / CM1, contribution after ads) from the ``profit_ladder`` query table or ``get_performance`` ``summary.profit``, cite the cost-coverage line ("94% of revenue has costs") and the ``margin_basis`` string next to it. A level withheld below 80% coverage is stated as withheld with its coverage, never as an amount, and no level is ever labelled CM2, CM3 or EBITDA. **Build on the report kit.** Read the ``kreel://report-kit`` resource: it holds the kit CSS, one HTML pattern per Dashboard v2 block (branding header, KPI tile with target chip, pace bar, grouped table with a "do not add" band, heatmap, Daily Overview, margin ladder, annotations, definitions and data-through footers, and the six empty/unavailable/error states) and its usage rules. Declare it with ``<link rel="kreel-kit">`` in ``<head>``; this tool replaces that tag with the inlined kit CSS and the Brand's accent colour, and returns the ``kit_version`` it recorded (null when the HTML does not declare the kit). Kit-based reports print cleanly to A4 or Letter. The authored document is rendered below it in a sandboxed frame. Inline CSS, JavaScript, and forms continue to work inside the report, but they cannot hide, rewrite, or navigate over the server-controlled data-trust section. Args: title: Human title for the report; the URL slug is derived from it. If a LIVE report with this exact title (normalized: case/whitespace/ punctuation-insensitive) already exists in this brand and ``supersedes`` is omitted, the call is refused rather than silently minting a ``-2``-suffixed sibling - see above. Always use the returned ``slug``/``url``, don't reconstruct the URL from the title. html: The full self-contained HTML document to host. Reference images by URL - do not base64-inline thumbnails (it bloats the payload). For creative thumbnails, use the ``get_creatives(fields="all")`` ``image_url`` / ``video_url`` in Kreel signed-URL form; those are snapshotted into the report at publish (verify the form - see above; a Meta CDN fallback is not frozen). CSS and JavaScript are allowed; the report is origin-isolated. brand: Brand name or id to publish under. Required when you can reach more than one brand; optional (auto-selected) when you can reach exactly one. Call ``list_brands`` to see options. supersedes: The ``slug`` or ``id`` of a previously published report to correct (typo, re-synced numbers, etc). That report is left untouched (still immutable, still individually viewable by its own record) but its **URL** now redirects (HTTP 302) to this new report - so a client who already has the old link keeps working. Chains of corrections resolve to the newest report, and deleting the newest report in the dashboard reverts the redirect (the prior report serves again). Omit this to publish an unrelated, standalone report as before. Must reference a report in the same brand as this publish - a report id/slug from another brand is rejected. Requires ``correction_note``. correction_note: Required with ``supersedes`` (at most 500 characters): one line saying what changed, e.g. "Refunds re-synced; revenue restated." The new version shows it as "Updated on {date}: {note}". view_id: Optional id of the saved view the report was built from (list_views / get_view). The data-trust section then links to it in the dashboard, for workspace members only. It must be a brand view or a workspace template: a private view or an Ask snapshot would leave other members with a dead link, so it is refused and nothing is published. draft: Save the report as a draft for human approval instead of publishing it now (see "Drafts and approval" above). rationale: For a draft: why this report should go out. Defaults to "Review this report before client delivery". evidence: For a draft: source references backing it (e.g. ``source_id``s).
publish_report
Run a JSON Query DSL expression against the normalized data model. inventory_cover exposes current canonical catalog stock, velocity and days_of_cover. No date filter or group_by; select/where/order_by/limit/cursor are supported. Metadata states the 28-day completed window and partial Meta tagger coverage. cost_coverage is one undated row per brand: catalog and trailing-90-day revenue cost coverage, uncovered variants and revenue_at_risk_90d. Load costs with set_product_costs. product_margin, variant_margin and margin_summary price retained history at current unit cost, excluding tax/shipping. Require bounded where.date, no group_by. Every row reports cost coverage; margin is null below 80% revenue coverage. margin_summary includes blended fulfillment and margin MER (not attributed ROAS). profit_ladder returns the margin levels (net_merchandise → cm1 → payment_fees → ad_spend → contribution_after_ads) per period: bounded where.date, where.grain eq total|day|week|month, no group_by. payment_fees_by_gateway returns fees per gateway by source (actual | rule | missing) with coverage: bounded where.date, no group_by. plan_vs_actual returns the budget plan (get_plan/set_plan) next to monthly actuals per line; where.version eq N picks a version (default active), where.date is optional, no group_by. product_returns and variant_returns expose bounded refund activity: require where.date with eq or gte/lte ISO dates. Select sold_units, refunded_units, sold_revenue, refunded_revenue, return_rate, revenue_return_rate, store_return_rate, baseline_delta (fraction), baseline_multiple and advertising. Identity filters, projection, ordering and pagination apply after the scoped weighted baseline; group_by is unsupported. Variant IDs are account/product local ("unknown" for absent IDs); canonical parent fields identify rollups. meta includes window, account coverage, currency, basis and unassigned totals. email_flow_types is one row per Klaviyo flow: flow_type (welcome, abandoned_checkout, ...), the source rule and a reason, overridden and live. email_flow_benchmarks is one row per (flow, metric) over the last 90 complete days: value, own_baseline (the 90 days before) and workspace_median of the flow type across this Workspace's Brands you reach, null below 5 (workspace_n). Rates are 0-1 fractions; no group_by or account. ga4_site, ga4_traffic, ga4_landing_pages and ga4_events are GA4 website rows for the brand's one selected property/stream (session-scoped acquisition; dates are the property's calendar). engagement_rate and purchase_session_rate recompute from summed numerators/denominators under group_by; a null purchase_session_rate means no purchase key event, not 0. purchase_revenue is unconverted, in the property `currency`, and never additive with Shopify or ad revenue. ga4_events counts view_item/add_to_cart/begin_checkout/purchase independently (not a funnel). For coverage, thresholding and warnings use get_performance(platform="ga4"). The DSL is the escape hatch for questions the canonical tools don't cover. Shape: { "from": "campaigns", "where": {"platform": {"eq": "meta"}, "spend": {"gt": 100}}, "select": ["platform", "spend", "roas"], "group_by": ["platform"], "order_by": [{"field": "spend", "dir": "desc"}], "limit": 50, "account": ["EU Store", "act_123"] } See `kreel://schema` for the full table/column/operator reference. Semantics to be aware of: - `daily_channel_summary` uses `channel`: meta_ads, google_ads, klaviyo_email, klaviyo_sms, shopify_store. `campaigns` and `adset_performance` use the ad-only `platform`: meta, google (ADR-0024). For example, {"from": "daily_channel_summary", "where": {"channel": {"eq": "meta_ads"}}, "select": ["channel", "spend"], "group_by": ["channel"]}. MCP integration selectors also called platform accept broader values; they do not add columns to provider-specific DSL tables. Neither dimension identifies a Connected Account or a Shopify sales channel. - No default time window - results span all synced history unless you add a `where` on the table's `date` column. - Top-level `account` (optional) narrows the query to named Connected Accounts - a string or list of display names, platform account ids, Kreel ids, or saved Account Group names (each group resolves to its member set); several values union into one set. Omit it for the blend of every account. An unknown value returns the structured `unknown_account` error naming the valid accounts and groups. - `limit` defaults to 1000 and is capped at 10000 for most tables. Known wide catalogs can define smaller page limits and compact default fields. Check `meta.selected_fields`, `meta.applied_limit`, and `meta.limit_clamped` for the exact projection and bound applied. - When more rows exist, `meta.has_more` is true and `meta.next_cursor` carries an opaque continuation. Repeat the same DSL fields, filters, ordering, and limit with `cursor` set to that value for the next page. - `meta.date_filter` echoes the effective date window: when your `where` targets the table's declared date column, it's `{"field": ..., <op>: ...}`; when it doesn't, it's `null` with `meta.note` set to "no date filter - full history". Tables with no declared date column at all (entity tables like `customers`, `products`) never claim full history - even if your `where` filters some other date-like column on them (e.g. `customer_created_at`), `meta.date_filter` stays `null` and `meta.note` only states that the table has no canonical date column, since the DSL cannot confirm your filter bounds row coverage the way a declared date_field does. - Setting `group_by` aggregates all other selected columns: measures are SUMmed, ratios recomputed from summed bases, and non-grouped text columns collapse to an arbitrary MAX - only put dimensions you group on in `select`. - Ratio columns (roas/cpa/ctr/cpm/cpc/open_rate/...) are computed from base measures and rounded; they are null when their denominator is 0. - Email performance tables accept filter-only `tag` for an exact, case-insensitive tag name and `name_contains` for a safe, case-insensitive source-name substring. Group by `source_type` to get one selected tag group's summed measures and weighted rates across a window. - Units differ by table: Google tables report `cost_micros` (currency × 1,000,000), and `revenue` on ad/campaign tables is platform-attributed conversion value, not store revenue - consult `kreel://schema`. - `store_revenue_net` is the refund-correct daily STORE-revenue series (same single source as `get_performance(entity_type='account')`). Use it - not `sum(daily_channel_summary.revenue)`, which is gross and mixes store with attributed ad/email revenue - for store net or MER math. It is a virtual table: it REQUIRES a bounded `date` filter (`gte`+`lte`, or `eq` for a single day) and does not support `group_by`; `net_revenue` is the headline column (gross_revenue - cross_period_refunds). - The response carries a `notices[]` array with advisory caveats (e.g. `daily_channel_summary.revenue` is gross, before refunds; or, when aggregating without grouping/filtering on `channel`, that store and platform-attributed revenue are non-additive and get summed together) - read it before interpreting results. Args: brand: Brand name or id to query. Required when you can reach more than one brand; optional (auto-selected) when you can reach exactly one. Call list_brands to see options. dsl: JSON Query DSL expression. view: Optional saved view or Ask snapshot id (list_views / get_view). It sets top-level `account` when the DSL has none, and bounds the table's date column to the view's window when `where` does not filter it (tables with no date column stay unbounded). The response echoes `view: {id, name, applied, not_applied}`.
query
Freeze a hypothesis, homogeneous typed subjects, metric expectation and inclusive dates. Review becomes eligible the following Brand-calendar day. Selectors support future naming matches. No holdout — observational comparison. See docs/test-ledger.md.
record_experiment
Persist a durable, reusable brand insight. With `id`, this UPDATES that existing learning (the merge path) — pass clear=["evidence"] to unset its evidence. Without `id` it creates a new one and returns `related` — the brand's existing active learnings on the same `topic`. If `related` is non-empty, prefer calling this again with that learning's `id` to consolidate instead of duplicating. `related` matches `topic` case-, spacing-, and underscore-insensitively (underscores are treated as separators) among active learnings, so re-cased, differently- spaced, or snake_case duplicates of the same topic still surface. Args: brand: Brand name or id. Required when you can reach more than one brand.
record_learning
Delete a competitor by name. This permanently removes the saved row. `name` is matched case-, spacing-, and underscore-insensitively (underscores are treated as separators), so "GLOSSIER" or "mcp_audit_test" removes "Glossier"/"MCP Audit Test". If nothing matches, the current competitor names are returned so you can retry with one that exists. Args: brand: Brand name or id. Required when you can reach more than one brand.
remove_competitor
Delete by case-insensitive name. Its codes become unmapped on the next read.
remove_partner
Compute the first verdict and linked learning atomically after the review date. Annotation cannot override computed numbers. Subsequent calls return the saved result.
review_experiment
Save a Brand-wide customer segment and return its definition and aggregates. A rule is an AND/OR tree: {"and": [...]}, {"or": [...]} or a condition {"feature", "operator", "value"}; at most 8 levels, 100 conditions and 100 values per list. Features: rfm_segment, rfm_recency_score, rfm_frequency_score, rfm_monetary_score, orders_count, net_ltv, aov (money: Base currency), days_since_last_order, overdue_status, first_product, product_ever_bought (values '<account_id>:<product_id>' from the catalog), acquisition_channel, country (ISO alpha-2, ZZ unknown), discount_code_share (0-1), accepts_marketing, shopify_tags. Numeric: eq ne gt gte lt lte in nin; categorical: eq ne in nin; boolean: eq ne; scalars also is_known / is_unknown (no value); product and tag sets: contains_any contains_all contains_none. An unknown value never matches, including ne and nin. Example "Lapsed VIPs": {"and": [{"feature": "rfm_monetary_score", "operator": "gte", "value": 4}, {"feature": "overdue_status", "operator": "eq", "value": "lapsed"}]}. Each person counts once across the Brand's stores (email hash, else store-local). Aggregates are size, 365-day refund-net revenue of orders dated in the window (Base currency; not Period Revenue), observed repeat share and RFM overlap. No members are returned and nothing is sent to any platform. Needs the `write` scope and the member role. Args: name: Unique per Brand, ignoring case (at most 120 characters). rule: The rule tree. brand: Brand name or id. Required when you can reach more than one brand. segment_id: Update this segment (saves the next version) instead of creating one. expected_version: Required with segment_id: the version you are editing.
save_customer_segment
Schedule a deterministic Kreel template to run on a Brand-local cadence. Needs the admin role on the Brand and a write-scoped credential owned by a user (a legacy key with no owner is refused, ``owner_required``). You become the job's owner: every run re-checks that you still hold admin on the Brand and that the workspace is subscribed, and skips otherwise. Templates (``list_agent_jobs`` lists them with their params): - ``weekly_growth_brief``: the Weekly Growth Brief for the last seven complete days. - ``findings``: new findings for yesterday; params ``kinds``, ``min_severity``. With ``output="message"`` it is an alert: nothing new sends nothing. - ``pacing``: month-to-date pace against the Brand's targets. Args: name: Label shown in the dashboard. template: One of the template keys above. There is no LLM runtime: ``runtime`` other than ``deterministic`` is refused (ADR-0034). schedule: ``{frequency: daily|weekly|monthly, hour: 0-23, weekday: 0-6 (weekly, 0 = Monday), day_of_month: 1-28 (monthly)}`` on the Brand reporting calendar. Missed hours are not backfilled. output: ``report`` publishes a hosted Report each run; ``message`` sends the brief to Slack and/or email only. destinations: ``{slack: bool, email_recipient_ids: [...]}``. Slack uses the Brand's alert webhook; recipients come from the Brand's digest list. A message job needs at least one. approval_mode: ``auto`` delivers each run; ``hold`` keeps each run as a draft until an owner/admin approves it in the dashboard. brand: Brand name or id. Required when you can reach more than one.
schedule_agent_job
Save the Brand's whole monthly budget plan as a new version. `start_month` is "YYYY-MM"; the plan covers it and the next 11 months. `lines` maps a line id to {"YYYY-MM": value or null}; omitted cells and nulls are blank. Replaces the whole grid: send every line you want kept (read get_plan first). Rules: spend and other_marketing and net_revenue >= 0; cm1 any number; new_customers a whole number >= 0; mer > 0; amounts in the Brand's Base currency. Any invalid cell writes nothing. A grid identical to the active version saves nothing (`changed: false`); otherwise the previous version stays readable. Owners and admins only (write scope). Audit-logged.
set_plan
Add or update a competitor by name. Partial — omitted fields are left unchanged on an existing competitor. To unset a field, name it in `clear` (e.g. clear=["handle"]). `name` is matched case-, spacing-, and underscore-insensitively (underscores are treated as separators), so "glossier" or "mcp_audit_test" updates an existing "Glossier"/"MCP Audit Test" (and refreshes its stored casing to the new input) rather than creating a duplicate. Args: brand: Brand name or id. Required when you can reach more than one brand.
set_competitor
Read or replace the Brand's custom creative tag dimensions in Kreel. With no `dimensions` and no `copy_from_brand` it returns the current schema (any role). Writing needs the owner or admin role. `dimensions` replaces the schema wholesale: a list of up to 5 objects `{"key", "label", "description"?, "values"}` where key is a lowercase slug (not a built-in tag or naming field) and values are 2-20 allowed strings. Pass [] to clear it. `copy_from_brand` copies another Brand's schema (same Workspace only); copying a Brand without one clears this one. Saving prunes stored values the new schema no longer allows (hand-set ones included) and makes every creative a re-tag candidate: Kreel's own AI tagger fills the new dimensions within the daily re-tag cap. Nothing changes in the ad accounts or anywhere outside Kreel. Values are then readable per creative (`get_creatives` `custom_tags`), groupable as `custom:<key>`, and queryable in the `creative_custom_tags` DSL table. The fixed AI dimensions and their vocabularies come back in `fixed_dimensions`.
set_creative_tag_schema
Create or update by case-insensitive name; omitted fields are preserved. Codes replace the saved list; pass [] to clear. All retained orders attribute immediately. Types: influencer, affiliate, podcast, other.
set_partner
Set current unit costs (COGS) for catalog variants, e.g. from a supplier price list. Each item: `unit_cost` (required; store currency, positive, at most 4 decimals; an explicit null or "" clears the manual override), `currency` (must equal the store currency; never converted), and one reference: `variant_id`, else `sku` (normalized), or `product_id` alone for a product-level cost. `account_id` disambiguates a SKU sold by several stores. Costs merge into the saved overrides. Returns per-row `status` (add/change/clear/unchanged/error) and the projected catalog coverage. Any row error writes nothing; call with dry_run=True first. Costs are current, not cost at sale: they re-state every margin window. Owners and admins only (write scope). Audit-logged.
set_product_costs
Change a Kreel dashboard; omitted arguments keep their value. Only Kreel members see dashboards; nothing leaves the Workspace. `tiles`, when given, replaces the whole tile list (same tile shape and rules as create_dashboard; a mixed-basis tile rejects the update and nothing changes). You can edit your own dashboards; an admin can edit any shared one. Only the creator can make a shared dashboard private. Args: dashboard_id: The dashboard id (from list_dashboards). brand: Brand name or id. Required when you can reach more than one brand. name: New name. description: New description. visibility: "private" | "brand" | "workspace". range: New saved range, {preset, days, start_date?, end_date?}. tiles: The complete new tile list.
update_dashboard
Update the Brand Profile typed anchors. Partial — only the fields you pass change. `price_tier` must be one of value/mid/premium/luxury; `margin_band` one of lean/moderate/healthy (invalid values are rejected). To unset a field, name it in `clear` (e.g. clear=["target_roas"]). Monthly targets (one recurring set per brand, paced month to date by get_brand_context / get_portfolio / get_weekly_growth_brief `pace`): `monthly_revenue_target` (store revenue, brand base currency), `monthly_spend_budget` (Meta + Google + TikTok spend), `target_mer` (store revenue / paid spend), `monthly_new_customers_target` (brand-new customers) and `target_new_customer_cac` (paid spend / new customers). Each must be greater than 0. `monthly_contribution_target` is the monthly contribution after ads (the profit ladder's CM1 minus Meta + Google + TikTok spend, current unit cost, brand base currency). It is not paced; the Margin page shows it in each month's contribution cell, prorated for partial months. It may be zero or negative (a break-even or investment plan). `lapse_threshold_days` (1-1095, default 180 when unset) is the customer-mix gap: a returning order placed at least this many days after the customer's previous order counts as `reactivated` in get_performance account summaries, the customer_mix_daily query table and the weekly brief. `pacing_tolerance_pct` (0-100, default 10 when unset) is the budget pacing tolerance: get_budget_pacing flags a platform off pace, and get_findings raises a `pacing` finding, when its projected month-end spend deviates from its budget plan line by strictly more than this percentage. Args: brand: Brand name or id. Required when you can reach more than one brand.
update_brand_profile
Create or replace a prose Knowledge Section (markdown). `key` must be one of: positioning, icp, competitive_landscape, brand_voice. One row per key — this replaces the section's content. Args: brand: Brand name or id. Required when you can reach more than one brand.
update_section
Retract one of this brand's proposed decisions before a human acts on it.
withdraw_decision
Kreel ChatGPT Plugin FAQ
How the directory, categories and Discoverability Score work.
Read the methodologyHow do I improve Kreel's ChatGPT Plugin discoverability?
The levers are the listing surface agents actually read: names, descriptions, keywords, tool metadata, and registry health. Which lever matters depends on where discovery breaks, which is what continuous measurement shows.
What are Kreel alternatives on ChatGPT?
As of 2026-10-29, Kreel competes with Ad Account Health Check, Ad Reporting by PaidSync.ai, Ad Superpowers, Adalysis, AdKit - Ads Manager, Adlane, Admiral, AdPage and 64 more in ChatGPT Ad Campaign Analytics & Reporting, ranked by public Discoverability Score.
Where is this profile measured?
This profile uses the geography attached to the latest public registry snapshot: US. Locale tags are intentionally omitted.