Kreel
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
- 17
- Geography
- US
The Primary Subcategory used for this profile’s headline score.
Other Subcategories where the Integration is visible.
ChatGPT Plugin Discoverability Score
ChatGPT organic discovery is not live yet
Kreel is tracked in the ChatGPT Plugin registry. Public organic-discovery measurement is not live for ChatGPT yet, so there is no score to publish today.
Get notified when your score goes live
Enter your work email and we’ll notify you when ChatGPT Plugin organic discovery scoring launches.
No spam. Unsubscribe any time.
Competing in ChatGPT Ad Campaign Analytics & Reporting
View CategoryHow the Discoverability Score works
Organic discovery scoring for Kreel on ChatGPT is not live yet. The score will use measured agent conversations when it launches.
Organic discovery scoring is pending. Your Plugin score will appear on this scale when measurement goes live.
FoundDiagnostic
Whether Claude found your Plugin in connector search. It must be Found before it can reach the picker, but the score counts picker appearances—not search results.
PickedMain score
How often your Plugin appeared in the picker, or Claude invoked it directly, across contested conversations. This percentage is the Discoverability Score; the headline number is rounded.
PositionedDiagnostic
What position your Plugin appeared in when it was shown in the picker. This shows prominence, but it does not affect the score.
17 tools agents can invoke
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 multiple subjects over a shared window. 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), `attributed_last_click` (an ad/email account or any campaign subject — last-click attributed value), or `blended` (a platform='all' account subject — store net plus attributed, which are different bases and are not additive). When `metric` is `revenue` or `roas` (both read the same revenue basis) 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). `subject.platform` is optional and defaults to 'all' (blended across every connected channel). Campaign subjects support 'meta', 'google', or 'all'; 'all' scans both paid-ad platforms. 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. metric: spend | revenue | roas | cpa | orders | conversions | ctr. 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 blended account therefore uses filtered store revenue with its full attributed data and full spend. 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
Flag channel and campaign metric changes with structured evidence. 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). Campaign ``orders`` means the platform conversion count and may be fractional for Google. 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 attributed last-click value elsewhere. Campaign revenue is attributed last-click. Revenue and ROAS evidence carries ``revenue_kind``. Campaign evaluation is bounded to the top 20 by baseline spend, plus bounded current-only launches. Args: 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, klaviyo_email, 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 seven-day growth brief. 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. The response includes refund-correct store revenue, refunds, paid spend, MER, paid-platform ROAS, 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
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. `last_sync_status` is the latest performance-relevant Sync Run's status, including failures. `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. `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. Individual fields may be null; check before use. - `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}`. `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
Fetch creative performance for Meta ads or Google assets. 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. **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`). **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 `hook_type`/`format_type` — 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). - 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); 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). 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 — 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" or "google". Google is metrics-only in v1. filters: Meta only: {"ad_id": "...", "campaign_id": "...", "adset_id": "...", "status": "ACTIVE"}. ad_id is the Meta ad id. Unknown keys are rejected. Apply in every mode, including rollup (e.g. hook breakdown within a campaign). Google v1 rejects filters explicitly. group_by: Meta-only rollup dimensions. Any of ["hook_type", "format_type"] can be combined. When set, returns blended performance per group under `groups` instead of per-creative rows. Other keys are rejected. Google v1 rejects group_by. 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/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). Ignored in rollup mode. 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. - `ad_spend`: Meta + Google spend from `daily_channel_summary` for the window (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. - `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. - `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. A workspace group also carries an optional `subtotal` (currency, revenue, ad_spend, orders, mer) 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: "revenue" | "spend" | "mer" | "revenue_delta". "revenue", "spend", and "mer" 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, 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. 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. 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" | "klaviyo" | "shopify". entity_type: "account" | "campaign" | "adset" | "ad" | "flow" | "email_campaign" | "search_term" | "keyword" | "product" | "product_group" | "asset_group". "adset"/"ad" support Meta Ads and Google Ads (platform="google" reads AdSetPerformance/GoogleAdPerformanceDaily; for Google, "adset" is an ad-group alias — ad-group discovery is via get_performance(entity_type="adset", platform="google"), not get_entities). For "adset"/"ad", `platform` must be "meta" or "google"; platform="all" returns Meta ads only (Google ads are NOT merged in) - query platform="google" separately to get the Google rows. Campaign rows carry `objective` — the raw platform objective / campaign type (Meta OUTCOME_SALES/OUTCOME_TRAFFIC/..., Google SEARCH/SHOPPING/ PERFORMANCE_MAX/...) — so non-sales campaigns (Sampling/Awareness) read in context; it is omitted when unknown. "search_term" and "keyword" are Google Ads only. "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. "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 "attributed_last_click" for every ad/email channel. Do NOT sum `revenue` across channels - the bases differ (store cash-flow vs overlapping last-click 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`. The same block also adds `aov` = store-net revenue / orders, and `blended_paid_roas` = summed ad-channel attributed revenue / total paid ad spend (an attribution-based complement to `mer`, not a replacement for it). Both `aov` and `blended_paid_roas` are omitted (key absent, not null) when their denominator is 0 - matching the roas/cpa omission convention used elsewhere in this tool's account/campaign rows. `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 ad/adset performance: {"campaign_id": "...", "campaign_name": "...", "adset_id": "...", "ad_id": "...", "name_contains": "..."}. `filters` apply ONLY to Meta ad/adset queries; for any other entity_type or platform they are ignored silently (no error). `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. Notes: - Derived ratios (roas, cpa, ctr, cpc, cpm, conversion_rate, open_rate, click_rate, bounce_rate, unsubscribe_rate, spam_rate, revenue_per_recipient) are OMITTED from a row when their denominator is 0 (undefined), not returned as 0. - 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: account rows use Shopify `orders`; campaign/adset/ad rows use platform conversions/purchases - so `cpa` is not directly comparable across 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
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.
list_brands
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" | "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 - klaviyo campaign/flow: tags as ``[{id, name}]`` 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. 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 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 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
Publish an HTML report and get back a shareable URL. **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``. 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.
publish_report
Run a JSON Query DSL expression against the normalized data model. 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", "campaign_name", "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: - 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.
query
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
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
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"]). 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
How do I improve a ChatGPT Plugin's 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-08-14, Kreel competes with AdKit, AdPage, Adspirer, Channel99, Data Bloo, Dynamoi, Flyweel, HYPD AI - Paid Ads & Analytics, Jepto, Lily Apple Ads, LinkedIn, LinkedIn Ads, LinkedIn Headline Rewriter, Markifact, Masha, Nexoya, Porter Metrics, Supermetrics, Whatagraph, Windsor.ai Facebook Ads 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.