Skip to content

Data tools

These are the read-only MCP tools your agent uses to answer analysis questions directly. Each one is built for a specific kind of question, and returns a shape an agent can reason about without you having to name tables or columns yourself. When a question doesn’t fit any of them, your agent can fall back to the open-ended Query tool.

You don’t call these tools yourself. You ask your connected agent a question in plain language, and it decides which tool (or tools) to call. The example ask under each tool is the kind of thing you’d actually type.

Every tool below except list_brands and get_portfolio takes an optional brand argument: a brand’s name or id.

  • If you can reach exactly one brand, it’s applied automatically and you can leave it out.
  • If you can reach more than one, you must pass it, or the tool returns an error listing the brands you can reach.

See Connect your agent for the full explanation of how brand access works.

The main read tools also take an optional account argument to scope a multi-account brand. It accepts one string or a list of strings; each value can be a connected account’s display name, platform account id, Kreel id, or a saved account-group name. Several values are combined into one account set.

  • Omit account to read the blend of every connected account in the brand.
  • Name one account to read it alone.
  • Pass a list to read a custom subset.
  • Pass a saved group name to read that group’s current members.

Multi-account and filtered responses carry an accounts manifest explaining which accounts were included, the brand’s base currency, conversions, accounts still backfilling, and timezone differences where relevant. An unknown account returns an unknown_account error with the valid choices instead of an empty result. See Multiple connected accounts for the dashboard workflow and account groups.

What it answers: Which brands can I query, and what are their ids?

Parameters: none.

Notable return fields:

  • brands: a list of {id, name, health} entries, one per brand you can reach. If a brand’s display name can’t be resolved, name falls back to its id.
  • health: the brand’s sync health, the same verdict the dashboard’s status pill shows.
    • state is the worst of its sources: reauth, then failed, stale, backfilling and fresh. It is none when nothing is connected.
    • sources lists each connected source with its status, a reason code, data_through, last_success_at, and backfilling ({verified_from, progress} while its history is still filling, otherwise null).
    • reauth and failed mean that source’s numbers stop at its data_through. Check health before you trust a brand’s recent figures.

Example ask: “List the brands I can access.”

What it answers: Spend, revenue, and efficiency metrics for accounts, campaigns, ads, products, and more, over a date range, optionally against the prior period. This is the tool your agent reaches for most.

Parameters:

| Name | Type | Default | Notes | |---|---|---|---| | platform | string | "all" | all, meta, google, tiktok, klaviyo, shopify, or ga4. See Website analytics (GA4) below for platform="ga4". | | entity_type | string | "account" | account, campaign, adset, ad, flow, email_campaign, search_term, keyword, product, product_group, or asset_group. Not every platform supports every type; see notes below. | | date_range | object | last 7 days, ending yesterday | {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} or {"days": N}. | | filters | object | none | Meta/TikTok adset/ad: campaign_id, campaign_name, adset_id, ad_id, name_contains (case-insensitive substring); TikTok also accepts adgroup_id as a synonym of adset_id. Klaviyo email_campaign/flow: send_channel ("email" or "sms"). Ignored for any other combination. | | metrics | list | all metrics | Narrows output to these metric keys; identity fields (id, name, status, and so on) always stay. Names absent from every row come back in ignored_metrics. An empty list returns identity fields only. | | limit | integer | 50 | Top N rows by spend (or revenue/cost, depending on entity). No “more rows” indicator; lower-ranked rows are dropped silently. | | compare_to_previous | boolean | false | Adds a vs_previous block per row, against the equal-length prior window, plus a top-level comparison_period: {"start", "end"} naming that resolved window. Only applies to account, campaign, and product. | | sales_channels | list | brand’s saved policy | Shopify source_name allowlist, e.g. ["web", "pos"]. Only valid for entity_type account/product with platform all/shopify. Overrides the brand’s saved exclusion policy for this call only. | | account | string or list | all accounts | Account name/id, list of accounts, or saved account-group name. | | brand | string | auto-selected if you reach one brand | Brand name or id. |

Notes on entity_type support:

  • adset and ad cover Meta, Google, and TikTok, but platform="all" only returns Meta rows for these two types. Query platform="google" or platform="tiktok" separately for Google or TikTok ad sets/ads (TikTok’s adset is its ad group).
  • search_term, keyword, product_group, and asset_group are Google Ads only (asset_group = Performance Max, product_group = Shopping); TikTok has no equivalent.
  • product is Shopify only: one row per product, revenue net of discounts and refunds, no shipping (there’s no defensible way to allocate shipping per product). See Contribution margin on product rows below.

Notable return fields:

  • revenue_kind is on account rows (one per row) and, for product, once at the top level of the result as store_net_merchandise (every product row shares that basis). Campaign, ad set, ad, flow, email campaign, and Google-only entity types don’t carry the field individually: their revenue is always platform-attributed under the provider’s attribution settings, per the note above.
  • With platform="all" and entity_type="account", a summary block adds a blended mer (store-net revenue over total paid spend) and aov (store-net revenue / orders, omitted when orders is 0). Both cover the full window independent of limit. mer and its numerator store_net_revenue are null with a reason when the account scope holds no Shopify store, never 0; mer null without a reason means there was no paid spend. There is no cross-platform ROAS: for each platform’s own efficiency, read the roas on its account row, identified by channel and revenue_kind: "platform_attributed". blended_paid_roas was removed in October 2026: it summed overlapping platform claims. Use mer, or each ad row’s roas.
  • objective on campaign rows carries the raw platform objective/campaign type - Meta’s OUTCOME_SALES/OUTCOME_TRAFFIC/…, Google’s channel type, or TikTok’s WEB_CONVERSIONS/PRODUCT_SALES/SHOP/… - so a non-sales campaign reads in context rather than looking broken.
  • Rate metrics (roas, cpa, ctr, cpc, cpm, conversion_rate, open_rate, click_rate, and others) are 0-1 fractions everywhere, and are omitted, not returned as 0, when their denominator is 0.
  • cpa’s denominator changes by level: Shopify orders at the account level, platform conversions at the campaign/adset/ad level. Don’t compare cpa across levels.

entity_type="adset"/"ad" with platform="tiktok" report purchases (TikTok website purchases, complete_payment) and revenue (their TikTok-attributed value, total_complete_payment_rate - a value despite the name), plus cta_conversions/vta_conversions (optimization-goal conversions by click-through/view-through attribution - never purchases, omitted when TikTok reports 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; the response’s semantics block restates these definitions. conversions on TikTok campaign rows and the tiktok_ads account row are the same website purchases. Do not compare raw TikTok figures against Meta or Google - the platforms attribute differently.

Example ask: “How did my paid channels perform over the last 7 days, compared to the week before?”

platform="ga4" reads Google Analytics 4 for the brand’s one selected property and web stream. It is a measurement source kept apart from Shopify and ad-platform revenue.

  • entity_type: website (or account) for the headline totals, traffic_source (session source / medium), channel_group, campaign (GA4 session campaign), landing_page, device, or shopping_activity.
  • Metrics: sessions, engaged_sessions, engagement_rate, purchase_sessions, purchase_session_rate, and ecommerce_purchases. Headline totals also carry ga4_purchase_revenue in the property currency, which is never added to store or ad revenue. Shopping activity returns event_count per event with a status: observed, not_observed_in_window, or never_observed.
  • filters takes GA4 dimensions only: channel_group, source_medium, device_category, landing_page, campaign_id, and campaign_name, plus min_sessions and, for landing pages, group_by (device_category, channel_group, or source_medium). A filter the report can’t take returns {"error": "incompatible_filter", "compatible_filters": [...]}. sales_channels and account don’t apply.
  • Every result carries a meta block: status (ok, empty, partial, syncing, no_coverage, needs_selection, not_connected, or permission_lost), the property and stream, exact windows, property vs brand time zone, currency, last sync, coverage, quality (thresholding, (other) rows, sampling, provisional days), and warnings. A null purchase_session_rate means the property has no purchase key event, not zero.

entity_type="product" rows carry contribution margin alongside revenue: cogs, refunded_cogs, contribution_margin, margin_pct (contribution_margin / net_revenue), cost_coverage_revenue/cost_coverage_units (the share of sold revenue/units with a resolvable unit cost), and margin_status (no_sales, reported, or insufficient_coverage - margin fields are null unless margin_status="reported", which requires at least 80% cost coverage). The result also carries top-level margin_basis (current unit cost, ex-tax/ex-shipping, manual overrides win) and a margin_summary blending the whole page, including spend (Meta + Google + TikTok ad spend over the window) and margin_mer (contribution_margin / spend). Current unit costs restate history - a cost change today changes yesterday’s reported margin too - so always check cost coverage before trusting a margin figure. See Contribution margin for the full basis definition and coverage rule.

Product rows also carry account_ids (every store a blended SKU sold in), and set unclustered: true for a row that couldn’t join canonical product identity (no variant SKU, or a deleted listing) - a duplicate of such a row reads as a separate product. A sku_conflicts array (top level) lists any SKU reused across genuinely different titles, which never merges automatically. excluded_sold_count (top level, when present) counts sales from products excluded in brand settings that are withheld from these rows.

What it answers: Lists of campaigns, ad sets, ads, flows, lists, segments, or products, with their structural fields (name, status, budget, trigger, and so on) rather than performance metrics. Optionally attaches a fixed 7-day performance rollup.

Supported combinations:

  • Meta: campaign, adset, ad
  • Google: campaign
  • TikTok: campaign, adset (TikTok’s ad group), ad
  • Klaviyo: campaign, flow, list, segment
  • Shopify: product

Parameters:

| Name | Type | Default | Notes | |---|---|---|---| | platform | string | required | meta, google, tiktok, klaviyo, or shopify. | | entity_type | string | required | Must be one of the supported combinations above for the chosen platform. | | filters | object | none | Equality filters, e.g. {"status": "ACTIVE"}. Keys are validated against that entity type’s real columns; an unrecognized key returns a valid_filters error instead of an unfiltered result. | | include | list | none | ["metrics_7d"] attaches a 7-day performance rollup, ending yesterday. Only for entity_type="campaign" (any platform) or Klaviyo entity_type="flow". ["content"] attaches the message content of each Klaviyo campaign or flow (see below). | | limit | integer | 50 | See ordering and total below. | | account | string or list | all accounts | Account name/id, list of accounts, or saved account-group name. | | brand | string | auto-selected if you reach one brand | Brand name or id. |

Notable return fields (per entity type, beyond id, name, and status):

  • Meta campaign: objective, effective_status, budgets, buying_type.
  • Meta ad set: campaign_id, effective_status, budgets, bid_amount, bid_strategy, optimization_goal, start/end times. daily_budget, lifetime_budget, and bid_amount are undated structural values in that Meta account’s native currency, not the brand’s base currency. Keep them separated by account when a selection spans currencies.
  • Meta ad: campaign_id, adset_id, effective_status.
  • Google campaign: channel_type, channel_sub_type, serving_status, is_removed. Google campaign budgets and bid strategies aren’t synced, so they never appear here. serving_status is raw Google API passthrough - its enum has no REMOVED member, since serving eligibility and campaign lifecycle are different Google dimensions. It’s omitted entirely when is_removed is true, rather than returning a value that contradicts it; treat status/is_removed as authoritative for lifecycle.
  • 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/modified times.
  • TikTok ad set (= TikTok ad group): campaign_id, operation_status, secondary_status, is_deleted, optimization_goal, optimization_event, promotion_type, budget, budget_mode, 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 didn’t report them, never inferred.
  • TikTok ad: campaign_id, adgroup_id, operation_status, secondary_status, is_deleted, ad_format, video_id (its fetchable creative asset; absent for Spark Ads without one), tiktok_item_id, identity_type, call_to_action, landing_page_url.
  • Klaviyo campaign: subject line, preview text, sender, send strategy and time, tags.
  • Klaviyo flow: trigger_type, tags.
  • Klaviyo list/segment: profile_count and related counts.
  • Shopify product: uses title instead of name, the only entity type with this naming quirk.
  • With include=["content"] on a Klaviyo campaign: content_status and, once captured, messages - one per message, so an A/B test has one per variation (named by label) - each with subject, preview_text, sender and the email’s plain text (an SMS’s body), capped at 2,000 characters with text_truncated when cut. Content is captured once, by the Klaviyo sync after the campaign sends: pending means sent but not captured yet, not_sent means it isn’t final, unavailable means Klaviyo returned no messages. The text is the template as a generic recipient sees it - personalization shows its default or a [name] placeholder, conditional blocks keep every branch, and anything that lives only in images isn’t in it.
  • With include=["content"] on a Klaviyo flow: messages - each flow message that has sent and been captured, with the same fields plus as_of. A flow message’s content is its current version in Klaviyo, re-checked about daily by the sync, so earlier sends may have used an earlier version; removed_in_klaviyo marks a message Klaviyo no longer has, whose content is its last captured version.
  • With include=["metrics_7d"] on a Klaviyo campaign or flow, the rollup’s open and click rates are 0-100 percentages, an exception to the 0-1 convention get_performance uses everywhere else.

TikTok status is the effective delivery state (secondary_status, e.g. CAMPAIGN_STATUS_ENABLE / CAMPAIGN_STATUS_DELETE), falling back to the advertiser’s own operation_status switch when TikTok reports no effective state. Deleted TikTok entities are kept so their history stays readable - filter {"is_deleted": false} to hide them.

Meta budgets (campaign vs. ad set): a campaign missing daily_budget/lifetime_budget while its ad sets carry them means Ad Set Budget Optimization (ABO) - read the budget off the ad sets. A campaign carrying a budget while its ad sets don’t means Campaign Budget Optimization (CBO) - read it off the campaign. Neither level carrying a budget means the budget data is unavailable (for example an ended or stale budget object), not a sync gap.

Ordering and total: total is the full pre-limit match count; count is just how many rows came back, so raise limit if total exceeds count and you need completeness. For platform meta/google/tiktok with entity_type="campaign", rows sort by 7-day spend descending (the same window as metrics_7d), ties broken by name ascending, then platform_id ascending as a final tiebreaker. Every other combination - Meta/TikTok ad sets 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.

Example ask: “List my active Meta ad sets.”

What it answers: One metric, compared across several subjects (channels, campaigns, or email campaigns/flows), over a single shared window - optionally each subject’s period-over-period change too.

Parameters:

| Name | Type | Default | Notes | |---|---|---|---| | subjects | list of objects | required | At least 2, unless comparison is set (then a single subject is allowed). Each is {"label", "platform"?, "entity_type"?, "id"?}. entity_type defaults to account (a full channel); or campaign (requires id, the platform’s campaign id); or email_campaign/flow (Klaviyo ids, with optional account_id and send_channel: "email" or "sms"). platform defaults to all for account subjects: the whole brand on one store basis, not a sum of channels (name the platform for platform-reported revenue, ROAS or conversions); campaign subjects support meta, google, tiktok, or all (scans every paid-ad platform). | | metric | string | "revenue" | spend, revenue, roas, cpa, orders, conversions, ctr, delivered, open_rate, click_rate, or revenue_per_recipient. The last four apply only to email_campaign/flow subjects (rates are unique/delivered fractions). For a platform: "all" account subject, orders is Shopify store orders, cpa is total paid-ad spend / store orders, and conversions returns value: null with a reason, because platform conversions overlap and are never summed across platforms. For campaign subjects, orders and conversions are the same value; there’s no true order count at campaign level. | | window | object | last 7 days, ending yesterday | {"days": N} or {"start", "end"}. | | comparison | object | none | {"mode": "previous" | "year" | "custom", "start"?, "end"?}. Derives each subject’s period-over-period value with strict coverage, adding a comparison block per subject (value, delta_pct, reason) and a top-level comparison_period. delta_pct is (current - previous) / previous, rounded to 4 places. | | sales_channels | list | brand’s saved policy | Same Shopify source_name allowlist as get_performance. Requires at least one account subject on platform all or shopify. | | account | string or list | all accounts | Account scope applied to every subject so the comparison stays consistent. | | brand | string | auto-selected if you reach one brand | Brand name or id. |

Notable return fields:

  • Each subject with a value carries a revenue_kind: store_net or platform_attributed, per the note above. Email subjects that resolve a value carry revenue_kind: "platform_attributed".
  • A platform: "all" account subject is store_net: store-net revenue and store orders over total Meta + Google + TikTok ad spend. Its roas is MER, the same figure as get_performance’s summary.mer, and carries basis: "mer"; its cpa carries basis: "ad_spend_per_store_order". With no Shopify store in the account scope, its revenue, roas, orders and cpa return value: null with a reason.
  • When subjects mix revenue kinds for a revenue, roas, orders or cpa comparison, the response adds a top-level notices entry warning the values aren’t additive.
  • A subject with no matching data returns {"label", "value": null, "reason": ...} instead of failing the whole call. An email subject whose id matches more than one connected account returns reason: "ambiguous id; provide account_id" with the candidate account_ids.
  • With comparison set, a subject’s comparison.value is withheld (null, with a reason) when the prior window’s history doesn’t cover the comparison period - never a delta against a partial sum.
  • Campaign lookups only scan the top ~1,000 campaigns by spend; if a campaign subject doesn’t set platform, a shared id across platforms can collide.

GA4 subjects ("platform": "ga4") compare the website or one segment, such as {"platform": "ga4", "entity_type": "device", "id": "mobile"}. The segment entity types are channel_group, traffic_source, device, and landing_page, and the metrics are sessions, engaged_sessions, engagement_rate, purchase_session_rate, and ecommerce_purchases. A comparison (previous, year, or custom) is withheld with a reason unless stored GA4 history covers the whole comparison window. GA4 subjects can’t be mixed with store or ad subjects in one call.

Example ask: “Compare ROAS between Meta and Google over the last 14 days.”

What it answers: Which channels, campaigns, products, or inventory moved outside a normal range this period, versus a baseline period.

Parameters:

| Name | Type | Default | Notes | |---|---|---|---| | window | object | last 7 days | The recent period to evaluate. | | baseline_window | object | the 14 days immediately before window | The comparison period. | | threshold | number | none | An extra minimum percent-change floor. Can only make detection stricter, never looser than the built-in thresholds. | | channels | list | all channels | Any of meta_ads, google_ads, tiktok_ads, klaviyo_email, klaviyo_sms, shopify_store. Any unrecognized id - alone or mixed with valid ones - returns a valid_channels error instead of a silently filtered or unfiltered result. | | severity | list | all severities | Subset of critical/warn to include, applied before the 10-alert cap. Omit to get all severities; an explicit empty list is a structured error, not “all”. An unrecognized value returns a valid_severities error. | | metrics | list | all metrics | Optional metric allowlist, including days_of_cover and velocity for the inventory checks below. | | account | string or list | all accounts | Account name/id, list of accounts, or saved account-group name. Useful for finding a market-level swing hidden by the full blend. | | brand | string | auto-selected if you reach one brand | Brand name or id. |

Notable return fields:

  • Evaluates spend, revenue, orders, ROAS, CPA, CTR, and CPM, both at the account level and for the top 20 campaigns by baseline spend. Campaign orders is a platform conversion count and can be fractional on Google.
  • Capped at the 10 highest-severity alerts; truncated says whether more matched. A window with more than 10 critical alerts can otherwise crowd out every warning - pass severity=["warn"] to retrieve them separately.
  • direction is literally up or down, not a judgment call: an “up” CPA is bad, but the field just reports which way the number moved.
  • A metric moving to or from zero is only flagged once it crosses a floor (spend: 50 account-currency units/day, orders: 5/day). Going from zero to active is labelled new_activity (no percent change is reported); going from active to zero is labelled stopped (reported as a flat -100%).
  • Revenue evidence carries revenue_kind, per the note above.
  • Every alert’s evidence carries basis: "daily_avg" for spend/revenue/orders (calendar-day averages) or "ratio" for ROAS/CPA/CTR/CPM (ratios of summed window bases, never averaged). window_value/baseline_value keep the same field names either way. daily_avg evidence also carries window_total/baseline_total (the period sums behind the averages) and window_days/baseline_days (the day counts actually divided by). Compare the totals only when the two day counts match; pct_change is always computed from the averages.

Inventory alerts: stockout_risk and dead_stock are evaluated on canonical products using current stock and trailing 28-completed-day velocity, independent of window/baseline_window. Linked ACTIVE Meta ad spend on a flagged product is best-effort (Meta only, tagger-dependent) - check inventory_metadata.advertising_coverage before treating it as complete.

Email deliverability alerts: Klaviyo campaign bounce/spam rates are compared against preceding account sends (thresholds: at least 1,000 deliveries and 5 bounces / 3 complaints, +0.005 / +0.0005 absolute rate increases, 25% / 50% relative increases for warn/critical). A spike from a measured zero baseline is new_activity; flow-series gaps (Klaviyo doesn’t measure flow bounces/spam) are omitted, never a measured 0.

Return-rate alerts: high_return_rate flags a product or variant whose return rate has moved against the recent store baseline (minimums: 30 units sold, 5 refunded, 100 store-wide units sold; +5 percentage points and 2x the baseline for warn, 3x for critical). Linked active Meta ad spend is canonical-product evidence over the last seven completed days - a different window than the return check itself.

Overdue-share alerts: overdue_share compares the latest weekly RFM run’s share of overdue customers with the run exactly one week earlier, independent of window/baseline_window. It warns above a 20% relative rise and is critical above 40%; it stays silent when either week is missing or the prior share is 0, and it is Brand-wide, so it is skipped when you name an account.

Example ask: “Did anything unusual happen with my ad performance this week?“

What it answers: Everything that needs attention for one brand, as one cited list across Kreel’s detectors: channel and campaign anomalies, email deliverability, stockout risk and dead stock, high return rates, and Meta creative fatigue. It adds no detection of its own; each finding wraps one detector alert, and the same list is available over REST at GET /api/v1/findings.

Parameters:

| Name | Type | Default | Notes | |---|---|---|---| | window | object | last 7 days | The recent period to evaluate. | | baseline_window | object | the 14 days immediately before window | Comparison period for anomaly and deliverability findings. | | kinds | list | all active kinds | Any of anomaly, deliverability, stockout_risk, dead_stock, high_return_rate, creative_fatigue. An empty list, an unknown kind, or a reserved kind returns a structured error. | | severity | list | both | Subset of critical/warn. An empty list or an unknown value returns a valid_severities error. | | account | string or list | all accounts | As for detect_anomalies; every detector reads the same account selection. | | page | string | none | The dashboard page whose “What changed” rail to reproduce: performance (every kind), ads (Meta, Google and TikTok anomalies), creative (creative fatigue), email (Klaviyo anomalies and deliverability) or store (stockout, dead stock and returns). kinds may only narrow the page’s own kinds. An unknown page returns a valid_pages error. | | platform | string | none | Only findings whose subject is on this platform: meta, google, tiktok, klaviyo or shopify. An unknown platform returns a valid_platforms error. | | brand | string | auto-selected if you reach one brand | Brand name or id. | | limit | integer | 50 | Capped at 200, most severe first. counts covers every match (after page and platform) and truncated says whether more matched. |

Notable return fields:

  • Each finding carries kind, severity, metric, direction, and a subject that is exactly one channel, campaign, ad, product, or variant, never a cross-platform blend.
  • effect is favourable, adverse or unknown: the metric registry’s good direction applied to direction, so revenue up or CPA down is favourable. Spend, and metrics outside the registry (days of cover, velocity, return rate), are unknown. severity stays magnitude only, so a large favourable move is still critical.
  • fingerprint is stable across reads and days for the same brand, kind, subject, metric, direction, and severity, so use it to deduplicate. It changes when a finding escalates from warn to critical.
  • window and baseline_window are the detector’s own windows: creative fatigue always compares seven days with the prior seven, inventory uses trailing 28-day velocity, and returns compare with the store in the same window.
  • evidence is the detector’s payload unchanged; inventory and returns metadata appear once under metadata.
  • revenue_kind is store_net or platform_attributed; never add the two.
  • Every source_ids entry resolves in sources, and deep_link opens the dashboard page with the brand and dates applied, and the finding’s baseline (when it has one) as the comparison window.
  • kinds lists every kind with its owning detector and its severity thresholds. sync_stale, pacing, proposal_pending, review_due, and verdict_landed are reserved until their detectors exist.
  • Creative fatigue has no severity tiers and is always warn.
  • magnitude is the finding’s size as a multiple of its kind’s own warn threshold (1 means exactly at warn), read from its evidence. It is null when the evidence lacks the value, such as new activity against a zero baseline.
  • With page, findings are ranked by severity, then magnitude (largest first, null last), then kind, and page is echoed in the response. This is exactly the list the dashboard page’s rail shows, so an agent sees what a person sees.

get_portfolio carries the same findings as per-brand {critical, warn} counts only.

Example ask: “What needs my attention on this brand this week?“

What it answers: Creative-level ad performance with fatigue diagnostics and AI-generated tags, for “what’s working” and “what’s fatiguing” questions.

Parameters:

| Name | Type | Default | Notes | |---|---|---|---| | platform | string | "meta" | meta, tiktok, or google. Google is metrics-only: only fields="core" is valid and group_by/filters are rejected. An empty Google page adds empty_reason: asset_metrics_not_synced (the per-asset sync has never populated the scoped accounts) vs no_rows_in_window (rows exist, just none in the window - widen date_range). | | filters | object | none | Meta and TikTok: ad_id, campaign_id, adset_id (TikTok: = adgroup_id), status, any AI tag dimension (hook_type, format_type, angle_category, offer_type, persona, talent_type, cta_type, visual_style, seasonality) and any Brand custom dimension as custom:<key>; a tag filter value of untagged matches creatives with no value. Unknown keys are rejected with an error, not ignored. | | fields | string | "core" | core (identity, tag labels, metrics, fatigue for Meta), tags (adds AI-written description, hook summary, angle, opening seconds, spoken/on-screen text, and pacing), or all (adds media URLs, headline, body, call to action). Meta and TikTok both support all three tiers; Google supports only core. | | group_by | list | none | Both platforms: hook_type, format_type, the seven taxonomy dimensions (angle_category, offer_type, persona, talent_type, cta_type, visual_style, seasonality) and each Brand custom dimension as custom:<key>. Meta adds asset and any configured naming-schema field; TikTok adds media_type. Combinable. Switches the response to a blended rollup per group instead of per creative; creatives with no value on a tag dimension form an untagged group, so group spend reconciles. | | rank_by | string | none | Requires group_by. Ranks rollup groups by a metric (roas, cpa, ctr, cpm, cpc, conversion_rate, spend, revenue, purchases on TikTok); cost metrics rank ascending, everything else descending. Incomplete/undefined groups are excluded and counted. | | min_spend | number | 0 | Requires group_by and rank_by. Non-negative base-currency spend floor for ranked groups. | | date_range | object | last 7 days, ending yesterday | {"start", "end"} or {"days": N}. Widen it to browse the full creative library, e.g. {"days": 365}. | | limit | integer | 200 | Hard ceiling of 1000 regardless of what you pass. Ignored in rollup mode. | | account | string or list | all accounts | Account name/id, list of accounts, or saved account-group name. | | brand | string | auto-selected if you reach one brand | Brand name or id. |

Notable return fields:

  • Only creatives with spend in date_range come back; a creative with zero spend in the window is left out entirely.
  • fatigue: hook_rate, hold_rate (null for non-video creatives), ctr, and a trend block (first half vs. second half of the window) for CTR, hook rate, and frequency. Meta only - TikTok rows carry lifecycle_stage/lifecycle_reason but no fatigue block.
  • lifecycle_stage is computed over a fixed trailing 30-day window, deliberately different from date_range, so a creative can read as a 30-day winner while showing no spend in a narrower window. The five performance stages, in fixed precedence (kill > fatiguing > scaling > winner > testing, first match wins), are judged against a per-audience-temperature efficiency bar. Two more values are not performance stages: paused (the ad’s effective status is not ACTIVE) and dormant (delivered in date_range but absent from the 30-day window - it ran and stopped, not a kill signal). kill replaces the retired dead value.
  • lifecycle_reason explains why a kill creative is kill (null for every other stage, including paused and dormant, which are self-describing): always "underperforming" now - still ACTIVE and delivering, but its recent AND cumulative ROAS are both below the brand’s kill threshold of the efficiency bar. An underperforming creative may still be actively spending. Also null on a kill creative whose ad metadata is missing (performance rows survive a deleted ad) - status is unknown there, so it’s judged purely on performance.
  • lifecycle_bar/lifecycle_bar_source disclose the exact efficiency bar a creative’s stage was judged against and where it came from (cohort_default, explicit_target, or account_fallback); both null for a dormant creative.
  • quality_ranking, engagement_rate_ranking, conversion_rate_ranking are Meta’s own diagnostic rankings; the key is absent when Meta hasn’t reported one yet.
  • first_delivery/age_days are measured over the full synced history, not date_range.
  • Creative revenue/roas/purchases are platform-attributed (Meta or TikTok), not Shopify-verified store revenue.
  • The response envelope includes total_creative_spend (every delivered creative matching the filter, not limited by limit) and omitted (count and spend dropped by the limit), so the two reconcile with what’s returned.
  • coverage reports {returned, tagged, untagged, with_media} (Meta) or {returned, total, truncated} (Google) over the returned page - a low tagged count means the Gemini tagger hasn’t reached every creative yet, not missing data.
  • Every Meta and TikTok row carries the AI tag dimensions (hook_type, format_type, angle_category, offer_type, persona, talent_type, cta_type - the call to action the creative itself makes, not the platform button - visual_style, seasonality) and custom_tags ({key: value} for the Brand’s custom dimensions). A null tag means the tagger has not reached the creative yet, or a value was cleared by hand. Values set by hand in the dashboard survive re-tagging.

set_creative_tag_schema reads or replaces the Brand’s custom creative tag dimensions: up to five, each a lowercase key, a label, an optional description and 2-20 allowed values. The Gemini tagger fills them in the same pass as the built-in dimensions, and they become groupable (custom:<key>), filterable and readable in the creative_custom_tags query table.

set_creative_tag_schema(
dimensions: list[dict] | None = None,
copy_from_brand: str | None = None,
brand: str | None = None,
) -> dict

| Parameter | Type | Description | |---|---|---| | dimensions | list of objects | Replaces the schema wholesale; [] clears it. Keys may not reuse a built-in tag or naming-schema field. | | copy_from_brand | string | Copies another Brand’s schema; both Brands must be in the same Workspace. Copying a Brand with no schema clears this one. | | brand | string | Brand name or id. |

With neither argument it returns the current schema and the built-in vocabularies (fixed_dimensions), for any role. Writing needs write scope and the owner or admin role. A save prunes stored values the new schema no longer allows (hand-set ones included) and makes every creative a re-tag candidate; re-tags run within a daily per-Brand, per-platform budget, after every first-time tag.

TikTok creatives are ad-grain, at parity with Meta: each delivered TikTok ad with a fetchable asset is one row, keyed on the ad’s video_id (else its first image_id - reuse across ads shares one creative_id and one stored copy). Rows carry the same shared hook_type/format_type tags and intelligence block as Meta (fields="tags"), Kreel-signed media (fields="all"), and spend/impressions/clicks/purchases/revenue/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.

Delivered ads with no fetchable asset (Spark Ads without a video_id, LIVE_CONTENT, CATALOG_CAROUSEL) are excluded from creatives and counted, with their spend, in without_creative - total_creative_spend covers only the rows a creative ranking can see, so report that share of spend as a coverage limit, never as zero creative activity. Each row carries both status (the raw TikTok operation_status advertiser switch, e.g. "ENABLE" - not guaranteed delivery) and secondary_status (TikTok’s effective delivery state, as get_entities reports it): an enabled ad under a paused campaign or 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, not the switch. lifecycle_stage/lifecycle_reason/lifecycle_bar/lifecycle_bar_source come from the brand’s TikTok stage-rule row (stage rules are saved per platform, so Meta and TikTok can use different thresholds) over the same fixed 30-day window as Meta’s. Do not compare raw TikTok and Meta ROAS - the platforms attribute differently.

Example ask: “Show me my Meta video creatives from the last 30 days, grouped by hook type, with fatigue signals.”

What it answers: How Klaviyo email is landing - engagement by inbox provider, bounce-type mix, and complaints - read from stored data, without contacting Klaviyo live.

Parameters:

| Name | Type | Default | Notes | |---|---|---|---| | date_range | object | 28 completed days | {"start", "end"} or {"days": N}, compared against the preceding period of equal length. | | account | string or list | all accounts | Account name/id, list of accounts, or saved account-group name. | | brand | string | auto-selected if you reach one brand | Brand name or id. |

Notable return fields:

  • Daily counts and rates by inbox provider (for example Gmail vs. Outlook), plus period bounce-type mix, stored message/flow evidence, and campaign send-date trends.
  • Rates and deltas are 0-1 fractions. Daily unique-open counts are per-day unique, not deduplicated across the whole period - don’t sum them as a period-unique total.
  • An incomplete or invalid rate comes back null with a reason, rather than a misleading 0.
  • Unknown inbox-provider ownership is retained (not dropped), and the response’s collection-coverage block explains when a measurement fell back to an account-wide total with no per-provider attribution - read that data through query on klaviyo_deliverability_daily with grain="account" instead, since it carries no provider breakdown.
  • Provider-level dates use event occurrence; campaign trends use send date - the two can disagree for the same period.

Example ask: “How is my Klaviyo email deliverability looking over the last month, by inbox provider?“

What it answers: A cross-brand rollup across every brand you can reach, for “what changed across my brands this week, who needs attention” in one call. Unlike every other tool on this page, it takes no brand argument: it always covers your entire reach.

Parameters:

| Name | Type | Default | Notes | |---|---|---|---| | date_range | string | "7d" | A day-count string ending in d, e.g. "7d", "30d", "90d". Ends yesterday. | | sort_by | string | "revenue" | revenue, spend, mer, or revenue_delta. revenue_delta sorts ascending (biggest decline first) and requires compare_to_previous=true. | | limit | integer | 25 | Hard-capped at 50. Applies per workspace, not overall; brands beyond it are dropped from the list but still counted in total_brands. | | compare_to_previous | boolean | true | The opposite default from get_performance’s compare_to_previous (compare has no equivalent parameter). |

Notable return fields:

  • Rows are grouped under workspaces. If you belong to more than one workspace, you get one group per workspace; an API key always yields exactly one group.
  • Each brand row: revenue (refund-correct store-net, same basis as get_performance’s store_net), orders, aov, ad_spend, mer, connected_platforms, and data_through (the oldest per-platform freshness date, so one stale connection makes the whole brand read stale). When the brand has no Shopify store in scope, revenue, orders, aov and mer are null, never 0, and store_reason says why.
  • Each brand row also carries source_status, its worst source freshness verdict (fresh, backfilling, stale, failed or reauth), and the response’s source_summary counts the returned brands per status. It is the same state list_brands reports as health.state. backfilling means every source is fresh but at least one is still filling its history.
  • currency is per brand, with no FX conversion: comparing raw revenue across brands in different currencies compares different units.
  • A workspace-group subtotal only appears when every brand in that group shares the same currency and the same resolved revenue policy; otherwise it’s left out rather than approximated, and notices says explicitly there’s no other cross-brand total anywhere in the response.
  • findings is the brand’s {critical, warn} finding counts for the window, counts only; call get_findings for the detail. It’s null when unknown (no connections, or a detector failed), never a reassuring 0.
  • unavailable appears only on a row where a block failed to compute on this call. It maps the block (findings, profit_ladder, cost_coverage or freshness) to computation_failed. That block’s fields are null, the row’s other figures are still valid, and the next call retries it.

Example ask: “Which of my brands lost the most revenue this month?“

What it answers: A deterministic, source-cited seven-day growth brief: refund-correct store revenue, refunds, paid spend, MER, per-platform ROAS, CAC, Klaviyo email revenue (SMS excluded), campaign movers, threshold anomalies, and fixed-rule next actions.

Parameters:

| Name | Type | Default | Notes | |---|---|---|---| | week_ending | string (date) | the last 7 complete reporting days | YYYY-MM-DD. Must be before today, so the brief only covers complete days. | | brand | string | required if you reach more than one brand | Brand name or id. |

Notable return fields:

  • Every figure in the brief carries a citation back to its source.
  • The brief explicitly warns that store revenue and attributed revenue are different bases and must not be added together.
  • creative_fatigue: independent Meta fatigue alerts, all-ad counts and daily spend, up to ten examples with resolvable source citations. Diagnostic windows end no later than yesterday; missing data is disclosed rather than silently skipped.
  • Includes inventory signals: the top three stockout risks, dead-stock count, and coverage counts, using current catalog stock and velocity ending at the brief’s end date (capped at yesterday). Linked ad spend is partial - Meta only, tagger-dependent.
  • Includes experiments due for review and verdicts landed in the window (see Experiments), each with a source link back to the immutable ledger. Reading the brief never triggers a review by itself.

Example ask: “Give me this week’s growth brief.”

What it answers: A deterministic, source-cited brief for one day on the brand’s reporting calendar, compared with the same weekday a week earlier and with the 28 complete days before it, plus a profit card for that day and month to date.

Parameters:

| Name | Type | Default | Notes | |---|---|---|---| | date | string (date) | yesterday | YYYY-MM-DD, brand-local. Must be before today, so the brief only covers a complete day. | | brand | string | required if you reach more than one brand | Brand name or id. |

Notable return fields:

  • metrics: store net revenue, orders, AOV, refunds, ad spend, MER and Klaviyo email revenue (SMS excluded). Each has same_weekday and baseline values and deltas. Money and counts compare with the 28-day total / 28 (daily_average); ratios with the 28-day aggregate numerator / denominator (aggregate_ratio).
  • comparison.same_weekday_reasons / baseline_reasons: why a comparison group (store, paid, email) is null - incomplete history is never read as zero.
  • profit.yesterday and profit.month_to_date: net revenue, orders, AOV, ad spend, MER, CM1 and contribution after ads. Margin levels carry an amount only when status is reported (80% cost coverage or more, current unit costs, excluding tax and shipping); otherwise the status and coverage explain why.
  • pacing and budget_pacing: month-to-date target and budget-plan pace through the date, when set.
  • findings (top five) and finding_counts from the findings registry.
  • freshness.stale: a connected platform more than three days behind, or unknown.
  • edition.status: pending until that day’s data processing has completed. Scheduled daily emails and Slack posts wait for complete.

Example ask: “How did yesterday compare with last Tuesday and the last four weeks?”

Email support@kreel.ai if a tool returns something you can’t explain.