# Xylo MCP Tool Reference

Auto-generated from the Xylo route registry (`packages/mcp-server/src/routes`). **300+ ad operations across Meta (Facebook & Instagram), Google Ads & TikTok, carried by 25 tools** (304 live routes).

Most tools are **dispatchers**: pick a route with top-level discriminators (`channel` / `resource` / `action` / `mode` / `lens` / `topic`) and nest every route argument under the single `params` object. `describe({tool, ...discriminators})` returns any route's exact schema; `search_tools({query})` finds routes by intent. Each route below shows its call signature and full usage description (the same text the model relies on).


## `accounts` — read · 14 routes

### `accounts({action:"read_brief", params:{…}})`

Read the saved memory / "account brief" for one ad account — the durable context you need to work the way this user expects. CALL THIS AT THE START of any task on an ad account, BEFORE creating or editing campaigns, ad sets, ads, budgets, or targeting, so you follow the account's established conventions instead of asking the user to repeat things they have already told you. Returns: (1) `brief` — the user's freeform description of the business/brand, and (2) `memory` — structured remembered facts keyed by a short slug, each { category, value, source, updated_at }. Remembered facts can include: naming conventions for campaigns / ad sets / ads, default budgets for new campaigns and ad sets, preferred geo targeting, audience and targeting preferences, frequently used product sets or catalogs, which existing campaigns are which (and where new ads usually go), brand voice, ad copy preferences, creative preferences, and anything else this account's assistant was told before. Also returns: (3) `brand` — the brand this account is attached to ({ id, name, attached_accounts, accounts_knowledge }, one attached_accounts entry per account on that brand); brand.accounts_knowledge carries each sibling account's brief and memory, so read it before asking the user for conventions the brand already taught another account, or null if the account is not attached to a brand yet. (4) `brand_profile` — structured brand identity (urls incl. the brand website, colors, fonts, anchor_line, voice, product_truths, proof_points, audience_notes, offer_notes) used to keep AI-generated creative on-brand; it is brand-level, shared across every account attached to the same brand, and updated with update({resource:"brand_profile"}). (5) `brand_assets` — the durable reference asset list (logos, exact product shots, style refs, screenshots) saved for the brand and shared the same way across every account attached to it; each asset's `id` is usable directly as a brand_asset_ids entry in media({action:"generate"}) so generated creative can reference the real logo/product. Works for Meta, Google, and TikTok ad accounts. Read-only; no side effects. If nothing has been saved yet, `memory` is {} — that just means this account has no prior context, so gather it from the user and save it with update({resource:"account_brief"}).

### `accounts({channel:"meta", action:"list", params:{…}})`

List or look up Meta ad accounts connected to the Xylo organization. USE THIS ANY TIME the user refers to an account by BRAND NAME (e.g. "Getmo.us", "Acme", "our client", "for <brand>") — pass the brand as `query` to resolve it to an ad_account_id. This is the entry point whenever you need an ad_account_id but the user only gave a name. Also use to discover which ad accounts are available before querying campaigns, ads, insights, Facebook Pages, Instagram posts, comments, audiences, or creatives. Returns account ID, name, currency, timezone, status, spend cap, and total amount spent. Also returns default_page_id, default_instagram_id, and default_catalog_id when configured — use these as page_id / instagram_actor_id / product_catalog_id when creating ads (e.g. Advantage+ Catalog Ads). Each account also includes `warehouse_enabled` (boolean) and `last_synced_at`. The response meta includes the org `plan`. IMPORTANT: `warehouse_enabled=true` means the local data warehouse is populated for this account — only then should you use warehouse({action:"query"}), warehouse({action:"schema"}), or insights({channel:"meta", lens:"ad_integrity"}). When warehouse_enabled=false, use live Meta tools (query({channel:"meta", resource:"campaign", mode:"list"}), insights({channel:"meta", lens:"ad"}), etc.). Optional query filters accounts whose name or id contains this substring (case-insensitive). Keywords: find account, resolve brand, look up account, account for <brand>, which account is <brand>, ad account id, act_ id, Meta account, Facebook ad account, Instagram ad account, page post, IG post, comments on latest post, brand name resolver, account discovery, warehouse enabled, sync status.

### `accounts({channel:"meta", action:"details", params:{…}})`

Get full details for a Meta ad account, including status, billing info, spend cap, balance, currency, timezone, business info, capabilities, funding source, and more. Use this for a comprehensive view of an account beyond what list_accounts returns.

### `accounts({channel:"meta", action:"activities", params:{…}})`

Get the activity log for a Meta ad account. Returns a chronological list of events such as campaign creation, budget changes, status updates, and policy actions. Useful for auditing changes and understanding account history.

### `accounts({action:"list_brands", params:{…}})`

List the organization's brands with the ad accounts attached to each, PLUS every connected account not yet attached to any brand (`unassigned`). Cross-platform: one brand groups its Meta, Google, and TikTok accounts together and they share one brand profile + reference assets. `has_profile` tells you which brands still need their profile built. CALL THIS FIRST when onboarding an org with multiple ad accounts, when the user asks to organize or group their accounts into brands, or before ingesting accounts — so you know which brand each account belongs to and what is still unassigned. Assign accounts with update({resource:"brand"}).

### `accounts({channel:"google", action:"list", params:{…}})`

List or look up Google Ads accounts (customers) connected to the Xylo organization. USE THIS FIRST for Google Ads: it is the no-argument discovery entry point — call it with no args to see every Google Ads account you have access to, with their customer_id values. USE THIS ANY TIME the user refers to a Google account by BRAND NAME (e.g. "Acme", "our client", "for <brand>") — pass the brand as `query` to resolve it to a customer_id. Every other Google Ads tool (campaigns, ad groups, ads, keywords, insights, assets, conversions, etc.) requires a customer_id — resolve it here first. This is the Google equivalent of accounts({channel:"meta", action:"list"}). Returns customer_id, manager_customer_id, name, status, billing_status, and `activated` (whether the account is activated on your plan). If `activated` is false, the account must be activated in the dashboard at https://xylomcp.com/dashboard before campaign/insight tools will return data. Keywords: list Google accounts, find Google Ads account, resolve brand to customer id, which Google account is <brand>, discover Google Ads customers, Google Ads customer id, customer id resolver, Google account discovery, MCC, account for brand.

### `accounts({channel:"tiktok", action:"list", params:{…}})`

List or look up TikTok Ads advertiser accounts connected to the Xylo organization. USE THIS FIRST for TikTok: it is the no-argument discovery entry point — call it with no args to see every TikTok ad account you have access to, with their advertiser_id values. USE THIS ANY TIME the user refers to a TikTok account by BRAND NAME (e.g. "Lambs&Ivy", "our client", "for <brand>") — pass the brand as `query` to resolve it to an advertiser_id. Every other TikTok tool (campaigns, ad groups, ads, insights, GMV Max, business centers, account info) requires an advertiser_id — resolve it here first. This is the TikTok equivalent of accounts({channel:"meta", action:"list"}). Returns advertiser_id, name, status, billing_status, and `activated` (whether the account is activated on your plan). If `activated` is false, the account must be activated in the dashboard at https://xylomcp.com/dashboard before campaign/insight tools will return data. Keywords: list TikTok accounts, find TikTok account, resolve brand to advertiser id, which TikTok account is <brand>, discover TikTok advertisers, TikTok ad account id, advertiser id resolver, TikTok account discovery, GMV Max account, account for brand.

### `accounts({channel:"tiktok", action:"details", params:{…}})`

Get TikTok Ads advertiser account info: name, currency, timezone, country, industry, balance. Use to check account details, currency, or status before creating campaigns. Resolve the advertiser_id first with accounts({channel:"tiktok", action:"list"}).

### `accounts({channel:"tiktok", action:"balance", params:{…}})`

Get the current ad account balance for a TikTok advertiser. Returns balance and currency. Useful for checking funds before campaign launch.

### `accounts({channel:"tiktok", action:"transactions", params:{…}})`

List the TikTok ad account billing transactions over a date range. Useful for finance / accounting / spend reconciliation.

### `accounts({channel:"tiktok", action:"business_centers", params:{…}})`

List TikTok Business Centers that the authenticated app/user can access. A Business Center holds multiple ad accounts, pixels, catalogs, and identities.

### `accounts({channel:"tiktok", action:"bc_advertisers", params:{…}})`

List all TikTok advertisers (ad accounts) inside a given TikTok Business Center.

### `accounts({channel:"tiktok", action:"bc_assets", params:{…}})`

List assets in a TikTok Business Center. Filter by `type`: ADVERTISER (ad accounts), CATALOG, DOMAIN, LEAD, PIXEL, STOREFRONT, TIKTOK_SHOP, or TT_ACCOUNT (TikTok account / identity).

### `accounts({channel:"tiktok", action:"identities", params:{…}})`

List TikTok identities used for Spark Ads (TT_USER for own/auth'd TikTok users, BC_AUTH_TT for BC-owned accounts). Spark Ads need an identity_id; this is how you find one. CUSTOMIZED_USER is being phased out.


## `query` — read · 88 routes

### `query({channel:"meta", resource:"ad", mode:"list", params:{…}})`

List ads in an ad set, or ACCOUNT-WIDE when adset_id is omitted (e.g. find ads by name across the whole account: omit adset_id + pass name_contains). Returns ad ID, name, status, delivery_status, creative details, and timestamps. Each ad includes creative_format — a derived media classification (image, video, carousel, mixed, catalog, or unknown) — so you can segment ads by creative type (e.g. analyze only image ads vs video ads) without inspecting each creative individually. RUN-STATE: read `delivery_status` (ACTIVE / PAUSED / COMPLETED / SCHEDULED / ...) — not raw `status` — to tell if an ad is actually live; an ad under a finished campaign/ad set keeps status=ACTIVE but delivery_status=COMPLETED. LEAN READS: to skip the large creative blob and just check run-state, pass fields:["delivery_status"] (or whatever you need). Supports filtering by status and by ad NAME (contains / does not contain, case-insensitive, multiple terms OR). Response includes paging: { next_cursor, has_more }. Pass next_cursor as the "after" param to get the next page.

### `query({channel:"meta", resource:"ad", mode:"get", params:{…}})`

Get details of a specific ad by ID. Returns name, status, ad set ID, creative details (headline, body, image, link, CTA), creative_format (derived media classification: image, video, carousel, mixed, catalog, or unknown), and timestamps. Placement-aware fields: creative.story_image_hash / story_video_id are set when the ad has a dedicated 9:16 story/reels asset (Placement Asset Customization). creative.creative_id is exposed so callers can drill into the underlying AdCreative via query({channel:"meta", resource:"creative", mode:"get"}) when they need the raw asset_feed_spec or asset_customization_rules. Use these fields when recreating an ad in another ad set to preserve story coverage. MULTI-MEDIA ADS: creative.media_sourcing_spec lists EVERY uploaded asset (Meta "multi-media": up to 10 images+videos in one ad), each flagged is_primary — so you see all media, not just the primary in object_story_spec. creative_format reads "mixed" when it combines images + videos. For per-media download URLs use query({channel:"meta", resource:"ad_media", mode:"get"}); for per-media performance + analysis use creative({channel:"meta", action:"analyze"}) with media_index. creative.branded_content is set when the ad is a partnership / branded content ad. When recreating a partnership ad with new text or media, pass this object through to create({channel:"meta", resource:"ad", mode:"single"}) / create({channel:"meta", resource:"ad", mode:"bulk"}) — otherwise the partner tag is dropped on the new ad. CLONE AN AD EXACTLY (swap only the media): the default response does NOT include the Advantage+ enhancement enrollment, the pinned catalog product set, or the site links — so cloning from it silently drops that config and the new ads fall back to the account defaults. To reproduce an ad faithfully, call with detail="full": the returned creative then also carries creative_features (the enrollment matrix as booleans), product_set_id, product_extensions, creative_features.site_links (the FULL list — pass all of them to clone exactly, not just the first few) — all shaped to drop STRAIGHT BACK into create({channel:"meta", resource:"ad", mode:"bulk"}). Read once with detail="full", swap image_hash / video_id (or story_image_hash) per new ad, and pass the rest of the creative through unchanged. detail="full" ALSO returns creative.advantage_plus_spec — the COMPLETE raw enrollment matrix (degrees_of_freedom_spec ~75 features, creative_sourcing_spec, destination_spec) exactly as Meta stores it. creative_features normalizes only the ~22 features the create tools can set; use advantage_plus_spec to SEE and DIFF everything (e.g. verify a clone matches the parent). It is READ-ONLY — set enhancements via creative_features / product_set_id / site_links, not by echoing it back. FORMAT DISPLAY OPTIONS: the Ads Manager "Format display options" panel (Single media / Carousel / Collection) maps to Meta`s format_transformation_spec. Read behavior (verified 2026-06-13): it comes back here ONLY for ads CREATED VIA THE API, and only the non-default entries (e.g. da_collection) — base formats single_media/carousel are implicit and never echoed. Ads configured in the Ads Manager UI do NOT expose it at all (returns nothing), so you can`t auto-clone a UI-built ad`s format options — pass format_transformation_spec EXPLICITLY in that case. Catalog (product_extensions) ads get it defaulted on automatically by the create tools.

### `query({channel:"meta", resource:"ad_video", mode:"get", params:{…}})`

Check the processing status of an uploaded ad video. Status values: "ready" (can be used in ads), "processing" (still encoding), "error" (upload failed). Poll every 3-5 seconds until "ready".

### `query({channel:"meta", resource:"ad_video", mode:"list", params:{…}})`

List videos uploaded to the ad account for use in creatives. Returns video IDs, names, and processing status.

### `query({channel:"meta", resource:"partnership_partner", mode:"list", params:{…}})`

List the partnership / branded_content partners this ad account has used in recent ads. Returns each unique partner once with their ID (the value that goes into creative.branded_content.partners[].id on create({channel:"meta", resource:"ad", mode:"single"}) / create({channel:"meta", resource:"ad", mode:"bulk"})), display name, Instagram handle (when applicable), and how many recent ads tagged them. USE THIS BEFORE create({channel:"meta", resource:"ad", mode:"single"}) WHEN: the user asks to make a partnership ad and refers to a partner by name or handle ("a partnership ad with @creatorbob", "tag XYZ Studio") — call this first to look up the ID without forcing the user to paste a numeric Facebook Page / Instagram Business Account ID by hand. The most-used partners are sorted first. If the partner the user mentioned does not appear here, they likely have not approved this brand for partnership ads yet (approval happens outside the API in Meta Business Suite); surface that to the user. Sorted by ad_count descending then by last_used_at descending. Results are cached for 60s.

### `query({channel:"meta", resource:"ad_media", mode:"get", params:{…}})`

Fetch the creative media URLs (image and/or video) for a Meta ad. View ad image, see ad creative, look at the ad, describe the image in this ad, download ad creative asset, download all carousel images. Works for all Meta ad formats including single image/video, carousel ads, and modern asset_feed_spec (PAC / Dynamic Creative) ads. Resolves image hashes to full-size CDN URLs via the ad account in a single batched lookup. Returns top-level fields: format ("single" | "carousel" | "multi_media"), image_url, image_hash, video_id, video_source_url, video_source_kind, video_permalink_url, video_thumbnail_url, video_source_reason. PAC STORY ASSETS: when the creative has a distinct 9:16 story/reel asset (Placement Asset Customization — creative.story_image_hash / story_video_id on ad reads), the response also resolves it: story_image_hash + story_image_url (downloadable CDN URL), or story_video_id + story_video_source_url / story_video_thumbnail_url / story_video_permalink_url. The top-level image_url/video_* fields always mirror the FEED asset; story_* fields are null when the ad has no separate story asset. Use both to download the full feed+story pair for an ad. CAROUSEL ADS: When the creative is a carousel (link_data.child_attachments has 2+ cards), the response also includes a carousel_cards array — one entry per card with link, name, description, image_hash, image_url, and (for video carousel cards) video_id + video_source_url. The top-level image_url/image_hash mirror the FIRST card for backward compatibility — for the full carousel use carousel_cards. format is "carousel" when this array is populated, "single" otherwise. MULTI-MEDIA ADS (Meta "multi-media": up to 10 images+videos in ONE ad, stored in media_sourcing_spec): format is "multi_media" and the response includes a media[] array (+ media_total) — one entry per uploaded asset with index, asset_type, is_primary, and the resolved video_source_url / image_url. The top-level fields mirror the PRIMARY asset; use media[] to see/download ALL of them. VIDEO RESOLUTION: Meta only returns a signed download URL (video_source_url, kind "source") for videos uploaded directly to the ad account`s library via /act_.../advideos. Page-post videos, shared videos, and creatives copied from another ad account return null for video_source_url. In that case, video_permalink_url points to the public Facebook page where the video can be viewed, and video_source_reason explains why download isn`t available — tell the user to download from Ads Manager / Meta Business Suite for those creatives. Video source URLs are short-lived — download immediately, do not store.

### `query({channel:"meta", resource:"adset", mode:"list", params:{…}})`

List ad sets within a specific campaign. Ad sets control targeting, budget, schedule, and optimization for a group of ads. Returns ad set ID, name, status, delivery_status, budgets, billing event, optimization goal, promoted_object, and schedule. Supports filtering by status and by ad set NAME (contains / does not contain, case-insensitive, multiple terms OR). OPTIMIZED EVENT: `promoted_object` names what each ad set actually optimizes for — the pixel + event (custom_event_type; or a custom event via custom_event_type=OTHER + custom_event_str; or a Custom Conversion via custom_conversion_id), app, page, or product set. ALWAYS read it before analyzing, ranking, or restructuring: judge ads on the event the ad set optimizes for, not on proxy metrics — an ad set optimizing for a custom event is misgraded by standard leads/purchases counts. RUN-STATE: read `delivery_status` (ACTIVE / PAUSED / COMPLETED / SCHEDULED / ...) — not raw `status` — to tell if an ad set is actually live; a finished ad set keeps status=ACTIVE but delivery_status=COMPLETED. TARGETING IS OPT-IN: the heavy `targeting` spec (which can carry thousands of ZIP codes) is NOT returned by default — add "targeting" to `fields` to include it. All budget values are in dollars, NOT cents. Response includes paging: { next_cursor, has_more }. Pass next_cursor as the "after" param to get the next page.

### `query({channel:"meta", resource:"adset", mode:"get", params:{…}})`

Get details of a specific ad set by ID. Returns name, status, delivery_status, campaign ID, budgets, billing event, optimization goal, bid strategy, promoted_object, and schedule. OPTIMIZED EVENT: `promoted_object` names what the ad set actually optimizes for — pixel + event (custom_event_type; a custom event via custom_event_type=OTHER + custom_event_str; or a Custom Conversion via custom_conversion_id), app, page, or product set. ALWAYS read it before judging or optimizing this ad set's ads — rank them on the optimized event (for a Custom Conversion, the insights actions[] row "offsite_conversion.custom.<custom_conversion_id>"), not on standard leads/purchases proxies. Read `delivery_status` (ACTIVE / PAUSED / COMPLETED / SCHEDULED / ...) — not raw `status` — to tell if the ad set is actually live; a finished ad set keeps status=ACTIVE but delivery_status=COMPLETED. TARGETING IS OPT-IN: the full targeting spec (geo / ZIPs / interests — potentially huge) is NOT returned by default. To get it, pass fields:["targeting"] (alongside any other fields you want). All budget values are in dollars, NOT cents.

### `query({channel:"meta", resource:"audience", mode:"list", params:{…}})`

List custom audiences for the ad account. Returns audience ID, name, type, approximate size (null if unavailable, e.g. for lookalike audiences), status (ready, expiring, out_of_date, too_small, or unknown), created time in ISO 8601, and detailed operation_status/delivery_status objects. Custom audiences are used in ad set targeting to reach specific groups of people.

### `query({channel:"meta", resource:"audience", mode:"get", params:{…}})`

Get details of a specific custom audience by ID. Returns name, type, approximate count (null if unavailable), status (ready, expiring, out_of_date, too_small, or unknown), created time in ISO 8601, and operation_status/delivery_status objects.

### `query({channel:"meta", resource:"budget_schedule", mode:"list", params:{…}})`

List budget schedules (high-demand periods) attached to a Meta campaign or ad set. Returns id, time_start, time_end (ISO 8601), budget_value, and budget_value_type (ABSOLUTE = fixed spend, MULTIPLIER = percentage of baseline budget).

### `query({channel:"meta", resource:"campaign", mode:"list", params:{…}})`

List Meta ad campaigns for the current ad account. Supports filtering by status (ACTIVE, PAUSED, DELETED, ARCHIVED), by campaign NAME (contains / does not contain, case-insensitive, multiple terms), and pagination. Returns campaign ID, name, status, delivery_status, objective, budgets, and schedule. Use this to get an overview of all campaigns, or to find campaigns whose name matches/excludes keywords (e.g. "cold", "hot", "Q3", "test"). RUN-STATE: read `delivery_status` (ACTIVE, PAUSED, COMPLETED, SCHEDULED, IN_PROCESS, WITH_ISSUES, ARCHIVED, DELETED) to tell whether a campaign is actually live — NOT raw `status`. Meta leaves a finished campaign at status=ACTIVE, so a completed campaign shows status=ACTIVE but delivery_status=COMPLETED. The `status` filter matches Meta's configured/effective status, so filtering status=ACTIVE still includes completed campaigns — check delivery_status to tell them apart. IMPORTANT: All budget values (daily_budget, lifetime_budget, budget_remaining) are returned in dollars, NOT cents. Response includes paging: { next_cursor, has_more }. Pass next_cursor as the "after" param to get the next page.

### `query({channel:"meta", resource:"campaign", mode:"get", params:{…}})`

Get details of a specific Meta ad campaign by ID. Returns name, status, delivery_status, objective, daily/lifetime budget, budget remaining, spend today, and schedule dates. Read `delivery_status` (ACTIVE / PAUSED / COMPLETED / SCHEDULED / ...) — not raw `status` — to tell if the campaign is actually live: a finished campaign keeps status=ACTIVE but delivery_status=COMPLETED. All budget values are in dollars (already converted from cents).

### `query({channel:"meta", resource:"catalog", mode:"list", params:{…}})`

List Meta product catalogs accessible to the ad account. Catalogs power Advantage+ Catalog Ads (formerly Dynamic Ads), Shop ads, and Collection ads. Returns catalog id, name, and product_count. Tries the ad account's owned_product_catalogs first, then falls back to catalogs owned by the user's connected Business Manager accounts. Returns an empty list if no catalogs are accessible.

### `query({channel:"meta", resource:"catalog_product", mode:"list", params:{…}})`

List or look up products in a Meta product catalog. Returns up to `limit` products with id, retailer_id, name, description, brand, price, currency, image_url, additional_image_urls, url, and availability. Use this to inspect what's in a catalog before targeting it in an Advantage+ Catalog Ads campaign, or to verify products are syncing correctly from your feed. PRODUCT-LEVEL INSIGHTS JOIN: pass `retailer_ids` to resolve the values returned by the `product_id` insights breakdown (insights({channel:"meta", lens:"campaign"}) etc. with breakdowns="product_id") to full product details — the breakdown reports the catalog `retailer_id` (feed/SKU ID), not the Facebook item id. Raw breakdown values like "45015356375236, Some Product Name" can be passed as-is; the ID before the first comma is extracted automatically. This is how you find WHICH PRODUCTS get the most spend/purchases in catalog ads. AI CREATIVE REFERENCES: products carry image_url + additional_image_urls (often multiple studio angles). To use them as generation references, pass the product id to media({channel:"meta", action:"stage_from_catalog"}) — do not download the URLs yourself.

### `query({channel:"meta", resource:"product_set", mode:"list", params:{…}})`

List product sets within a Meta product catalog. USE THIS when the user wants to enable product extensions on an ad and pin a specific product set (e.g. "show NEW ARRIVALS underneath this ad"). Returns id, name, product_count, and filter for each set. Pass the chosen set's id as `creative.product_set_id` in create({channel:"meta", resource:"ad", mode:"single"}) along with `creative.product_extensions: true`. If the user just wants product extensions enabled and is happy to let Meta auto-match the catalog from the ad's URL, skip this and just set product_extensions=true with no product_set_id.

### `query({channel:"meta", resource:"creative", mode:"list", params:{…}})`

List ad creatives for the ad account. Creatives contain the visual and text content of ads (images, headlines, body text, links, call-to-action buttons). Note: In the Xylo API, creatives are embedded in ads. This tool fetches creative assets uploaded via the creatives/upload endpoint.

### `query({channel:"meta", resource:"creative", mode:"get", params:{…}})`

Get details of a specific ad creative by ID. Returns the full creative specification including images, text, links, and call-to-action configuration. Also returns asset_feed_spec (with images, videos, bodies, titles, link_urls, call_to_action_types, and asset_customization_rules) for Placement Asset Customization / Dynamic Creative ads — use this to read the raw per-placement asset mapping (feed vs. story/reels) and the adlabel→placement rules. Returns degrees_of_freedom_spec.creative_features_spec — the Advantage+ creative enhancements actually enrolled on the ad, each as { enroll_status: OPT_IN | OPT_OUT } — plus creative_sourcing_spec (product_extensions catalog + site links). Use these to verify which Advantage+ enhancements a created ad really has.

### `query({channel:"meta", resource:"instagram_publishing_limit", mode:"get", params:{…}})`

Check the current publishing rate limit for an Instagram account. Shows how many of the 100 allowed posts in the current 24-hour window have been used and how many remain. Use this before bulk publishing to avoid hitting the limit.

### `query({channel:"meta", resource:"instagram_media", mode:"get", params:{…}})`

Shortcut for "the latest Instagram post" — resolves everything in one call. Returns the most recent published media for an Instagram account plus its comments, so you don't need to chain query({channel:"meta", resource:"instagram_account", mode:"list"}) → query({channel:"meta", resource:"instagram_media", mode:"list"}) → publish({channel:"meta", resource:"comment", action:"moderate"}). Use this FIRST for any prompt like "comments on our latest IG post", "how did the last reel do", "what are people saying about our newest post", etc. If the user refers to an account by BRAND NAME (e.g. "Getmo.us", "our client Acme"), first call accounts({channel:"meta", action:"list"}) with `query` set to that name to resolve the ad_account_id — then pass it here as ad_account_id. If `ig_account_id` is omitted, falls back to the ad account's `default_instagram_id` (set in the Xylo dashboard). Pass `ig_account_id` explicitly if the user is asking about a specific brand whose IG account is not the default for the current ad account. Keywords: latest Instagram post, IG post, Instagram comments, latest reel, latest story, IG engagement, what are people saying, reactions on our IG, how did the post do.

### `query({channel:"meta", resource:"lead_form", mode:"list", params:{…}})`

List Lead Gen forms attached to a Facebook Page. Returns each form's id, name, status (ACTIVE, PAUSED, DELETED, ARCHIVED), and created_time. Use the returned form id with query({channel:"meta", resource:"lead_submission", mode:"list"}) to retrieve the actual leads (names, emails, phone numbers, etc.) submitted to that form. Requires leads_retrieval permission on the Meta access token and the Page token tied to the Page that owns the form.

### `query({channel:"meta", resource:"lead_submission", mode:"list", params:{…}})`

Retrieve submitted leads for a specific Lead Gen form. Returns each lead's id, created_time, and a fields object mapping question names to answers (e.g. { "full_name": "Jane Doe", "email": "jane@example.com", "phone_number": "+15551234" }). Use query({channel:"meta", resource:"lead_form", mode:"list"}) first to find the form_id. Requires leads_retrieval permission on the Meta token.

### `query({channel:"meta", resource:"uploaded_media", mode:"list", params:{…}})`

List media assets (images and videos) staged in the Xylo dashboard for an ad account. Assets are kept for 24h after upload. By default returns metadata + preview URLs (small payload). Set previews="inline" to receive compressed image previews inside MCP image blocks — useful when you need to visually inspect a small batch. Use the `ids` filter to recover hashes after a dropped finalize stream. Each asset has two usable references: (1) `meta_ref` — the image_hash (images) or video_id (videos) for create({channel:"meta", resource:"ad", mode:"single"}). Null until the asset has been pushed to Meta with media({channel:"meta", action:"finalize"}). (2) `preview_url` — a direct Vercel Blob CDN URL, fetchable by Meta's Graph API scraper. When a user says "use the creative I uploaded", "group my uploaded images", or "create ads with my uploaded assets," call this tool first. Assets staged together with a group (e.g. via media({channel:"meta", action:"upload_directory"}) or the dashboard) appear grouped here. To group images that belong together, use media({channel:"meta", action:"group"}). To name a group, use media({channel:"meta", action:"rename_group"}).

### `query({channel:"meta", resource:"page_post", mode:"get", params:{…}})`

Shortcut for "the latest Facebook Page post" — resolves everything in one call. Returns the most recent published post for a Page plus its comments, so you don't need to chain query({channel:"meta", resource:"page", mode:"list"}) → query({channel:"meta", resource:"page_post", mode:"list"}) → publish({channel:"meta", resource:"comment", action:"moderate"}). Use this FIRST for any prompt like "comments on our latest Facebook post", "how did the last Page post do", "what are people saying on our Page", etc. If the user refers to an account by BRAND NAME (e.g. "Getmo.us", "our client Acme"), first call accounts({channel:"meta", action:"list"}) with `query` set to that name to resolve the ad_account_id — then pass it here as ad_account_id. If `page_id` is omitted, falls back to the ad account's `default_page_id` (set in the Xylo dashboard). Pass `page_id` explicitly if the user is asking about a specific Page that is not the default for the current ad account. Keywords: latest Facebook post, FB page post, Facebook comments, Page comments, latest post comments, reactions on our post, how did our post do, what are people saying.

### `query({channel:"meta", resource:"page", mode:"list", params:{…}})`

List Facebook Pages the authenticated user manages. Call this FIRST for any prompt like "show me my Facebook Pages", "what Pages do I manage", "list my Pages", etc. NO ad_account_id is required for the default user-listing path — just call it directly. Optional `query` filters by name or id (case-insensitive substring). Set source=ad_account + ad_account_id to list Business Manager pages promotable by a specific ad account (promote_pages endpoint) — use this when you need the broader BM scope rather than the user-owned set. Keywords: list Facebook Pages, my Pages, Pages I manage, Page discovery, admin Pages.

### `query({channel:"meta", resource:"pixel", mode:"list", params:{…}})`

List Meta Pixels (Facebook Pixels) connected to the ad account. Pixels are used to track website events and power conversion-based campaigns. Returns pixel ID, name, last fired time, and automatic matching settings.

### `query({channel:"meta", resource:"pixel", mode:"get", params:{…}})`

Get details of a specific Meta Pixel by ID. Returns the pixel name, base code snippet for installation, last fired time, creation time, and automatic matching configuration.

### `query({channel:"google", resource:"ad_group", mode:"list", params:{…}})`

List Google Ads ad groups within a specific campaign. Ad groups organize ads and keywords under a campaign. Returns ad group ID, name, status, CPC bid, and campaign association. Note: pagination is not currently supported — all matching results up to the limit are returned in one response.

### `query({channel:"google", resource:"ad_group", mode:"get", params:{…}})`

Get details of a specific Google Ads ad group by ID. Returns name, status, CPC bid, campaign ID, and ad group type.

### `query({channel:"google", resource:"ad", mode:"list", params:{…}})`

List Google Ads within a specific ad group. Returns ad ID, name, status, type, headlines, descriptions, and final URLs. Supports filtering by status and pagination. Note: pagination is not currently supported — all matching results up to the limit are returned in one response.

### `query({channel:"google", resource:"ad", mode:"get", params:{…}})`

Get details of a specific Google Ads ad by ID. Returns the ad name, status, type, headlines, descriptions, final URLs, and creation time.

### `query({channel:"google", resource:"asset", mode:"list", params:{…}})`

List Google Ads assets (ad extensions) for the current account. Filter by type to see sitelinks, callouts, structured snippets, call extensions, or image assets.

### `query({channel:"google", resource:"campaign_asset", mode:"list", params:{…}})`

List assets linked to a specific Google Ads campaign. Returns asset IDs, field types, and link status. Filter by field_type to see only sitelinks, callouts, etc.

### `query({channel:"google", resource:"audience", mode:"list", params:{…}})`

List Google Ads remarketing audiences (user lists) for the current customer account. Returns audience ID, name, type, member count, status, and creation time.

### `query({channel:"google", resource:"audience", mode:"get", params:{…}})`

Get details of a specific Google Ads remarketing audience by ID. Returns the audience name, type, member count, status, and creation time.

### `query({channel:"google", resource:"budget", mode:"list", params:{…}})`

List Google Ads campaign budgets for the current account. Returns budget ID, name, amount (in dollars), delivery method, status, and how many campaigns reference each budget. Note: pagination is not currently supported — all matching results up to the limit are returned in one response.

### `query({channel:"google", resource:"budget", mode:"get", params:{…}})`

Get details of a specific Google Ads campaign budget by ID. Returns the budget name, amount in dollars, delivery method, status, and reference count.

### `query({channel:"google", resource:"campaign", mode:"list", params:{…}})`

List Google Ads campaigns for the current customer account. Supports filtering by status (ENABLED, PAUSED, REMOVED) and pagination. Returns campaign ID, name, status, type, budgets, and schedule. Note: pagination is not currently supported — all matching results up to the limit are returned in one response.

### `query({channel:"google", resource:"campaign", mode:"get", params:{…}})`

Get details of a specific Google Ads campaign by ID. Returns name, status, campaign type, daily budget, start/end dates, and bidding strategy.

### `query({channel:"google", resource:"conversion_action", mode:"list", params:{…}})`

List Google Ads conversion actions for the current customer account. Returns conversion action ID, name, status, type, category, counting type, lookback windows, and value settings.

### `query({channel:"google", resource:"conversion_action", mode:"get", params:{…}})`

Get details of a specific Google Ads conversion action by ID. Returns name, type, category, counting type, lookback windows, value settings, and attribution model.

### `query({channel:"google", resource:"experiment", mode:"list", params:{…}})`

List Google Ads experiments (A/B tests) for the current account. Returns experiment ID, name, status, type, date range, and associated campaign.

### `query({channel:"google", resource:"experiment", mode:"get", params:{…}})`

Get details of a specific Google Ads experiment by ID. Returns name, description, status, type, goals, date range, and traffic split.

### `query({channel:"google", resource:"keyword", mode:"list", params:{…}})`

List keywords within a specific Google Ads ad group. Keywords trigger ads when users search for matching terms. Returns keyword text, match type, status, CPC bid, and quality score. Note: pagination is not currently supported — all matching results up to the limit are returned in one response.

### `query({channel:"google", resource:"keyword_ideas", params:{…}})`

Keyword research via Google Ads Keyword Planner — discover NEW keywords and their search volumes. Seed with keywords, a landing-page URL, or both. Returns each idea with avg_monthly_searches, competition (LOW/MEDIUM/HIGH), competition_index (0–100), and top-of-page bid range (dollars). Defaults to United States + English + Google Search. Low-spend accounts may see bucketed volume ranges.

### `query({channel:"google", resource:"asset_group", mode:"list", params:{…}})`

List asset groups for a Google Ads Performance Max campaign. Asset groups contain the targeting signals, assets, and final URLs for PMax campaigns. Requires a campaign_id.

### `query({channel:"google", resource:"asset_group", mode:"get", params:{…}})`

Get details of a specific Google Ads asset group by ID. Returns name, status, campaign, final URLs, and display URL paths.

### `query({channel:"google", resource:"asset_group_asset", mode:"list", params:{…}})`

List assets linked to a specific asset group. Returns asset IDs, field types (HEADLINE, DESCRIPTION, MARKETING_IMAGE, etc.), and link status.

### `query({channel:"google", resource:"user_list", mode:"list", params:{…}})`

List Google Ads user lists (audiences) for remarketing and customer match. Returns list ID, name, type, search audience size, membership status, and match rate.

### `query({channel:"google", resource:"user_list", mode:"get", params:{…}})`

Get details of a specific Google Ads user list by ID. Returns list name, type, audience size, membership status, and match rate percentage.

### `query({channel:"google", resource:"change_history", mode:"list", params:{…}})`

Get the change history for a Google Ads account. Shows who made what changes and when. Requires a date range. Optionally filter by resource type (CAMPAIGN, AD_GROUP, AD, etc.).

### `query({channel:"google", resource:"recommendation", mode:"list", params:{…}})`

List Google Ads optimization recommendations for the account. Recommendations suggest improvements to campaigns, budgets, keywords, and ads. Filter by type for specific suggestions.

### `query({channel:"google", resource:"negative_keyword", mode:"list", params:{…}})`

List negative keywords for a Google Ads campaign. These are search terms that prevent your ads from showing.

### `query({channel:"google", resource:"negative_keyword_list", mode:"list", params:{…}})`

List shared negative keyword lists in the Google Ads account. These lists can be linked to multiple campaigns.

### `query({channel:"tiktok", resource:"ad_group", mode:"list", params:{…}})`

List TikTok Ads ad groups within a specific campaign. Ad groups control targeting, budget, schedule, and optimization for a group of ads. Returns ad group ID, name, status, optimization goal, budget, and schedule.

### `query({channel:"tiktok", resource:"ad_group", mode:"get", params:{…}})`

Get details of a specific TikTok Ads ad group by ID. Returns name, status, campaign ID, optimization goal, placement type, budget, bid price, and schedule.

### `query({channel:"tiktok", resource:"campaign", mode:"list", params:{…}})`

List TikTok Ads campaigns for the current advertiser account. Supports filtering by status (ENABLE, DISABLE, DELETE) and pagination. Returns campaign ID, name, status, objective, budget, and budget mode.

### `query({channel:"tiktok", resource:"campaign", mode:"get", params:{…}})`

Get details of a specific TikTok Ads campaign by ID. Returns name, status, objective, budget, budget mode, and creation time.

### `query({channel:"tiktok", resource:"spark_post", mode:"list", params:{…}})`

List TikTok posts (item_ids) authorized for Spark Ads under a given identity. Use these item_ids when creating a Spark Ad creative.

### `query({channel:"tiktok", resource:"ad_video", mode:"list", params:{…}})`

List videos in the TikTok Ad account library. Filter by video_ids or material_ids.

### `query({channel:"tiktok", resource:"ad_image", mode:"list", params:{…}})`

List images in the TikTok Ad account library.

### `query({channel:"tiktok", resource:"ad_video", mode:"get", params:{…}})`

Get transcoding status of one or more uploaded TikTok ad videos. Videos must finish processing before being usable in ad creatives.

### `query({channel:"tiktok", resource:"video_cover", mode:"list", params:{…}})`

Get auto-generated cover thumbnails for a TikTok ad video. Returns multiple cover URIs to choose from.

### `query({channel:"tiktok", resource:"smart_plus_campaign", mode:"list", params:{…}})`

List TikTok Smart+ auto-optimized campaigns (the Advantage+ / PMax equivalent). Smart+ replaced SPC after 2026-03-31.

### `query({channel:"tiktok", resource:"gmv_max_campaign", mode:"list", params:{…}})`

List TikTok GMV Max campaigns — automated TikTok Shop campaigns that bid toward an ROI (ROAS) target to maximize gross merchandise value (GMV). Filter by promotion_types (PRODUCT_GMV_MAX, LIVE_GMV_MAX).

### `query({channel:"tiktok", resource:"gmv_max_campaign", mode:"get", params:{…}})`

Get a single TikTok GMV Max campaign by ID, including its status, objective, and ROI protection status.

### `query({channel:"tiktok", resource:"gmv_max_store", mode:"list", params:{…}})`

List the TikTok Shops an ad account can use for GMV Max campaigns. Returns each shop's store_id, store_authorized_bc_id, store_name, store_status, and is_gmv_max_available. USE THIS FIRST to obtain the store_id + store_authorized_bc_id required by query({channel:"tiktok", resource:"gmv_max_post", mode:"list"}) and query({channel:"tiktok", resource:"gmv_max_creator", mode:"list"}). Keywords: GMV Max shops, TikTok Shops for GMV Max, store id, business center id for shop, which shops support GMV Max, GMV Max store list.

### `query({channel:"tiktok", resource:"gmv_max_creator", mode:"list", params:{…}})`

List the creators/identities (TikTok accounts) associated with a TikTok Shop and whether each is available for Product or LIVE GMV Max campaigns. This is the roster behind the "Creators" tab of Ads Manager GMV Max creative reporting: user_name (the real @handle), display_name (label), profile_image, identity_id, identity_type, product_gmv_max_available, live_gmv_max_available, is_running_custom_shop_ads, unavailable_reason. When TikTok returns a user_name, Xylo adds a derived profile_url (https://www.tiktok.com/@<user_name>) — this is the only GMV Max endpoint that exposes the actual @handle (present for BC-authorized creators; the shop's own TTS_TT identity has no user_name). The report and posts tools only carry display names, not @handles, so a profile link can't be built there. Get store_id + store_authorized_bc_id from query({channel:"tiktok", resource:"gmv_max_store", mode:"list"}). NOTE: for per-creator GROSS REVENUE / performance, use insights({channel:"tiktok", lens:"gmv_max"}) grouped by item_id with tt_account_name — this tool lists who the creators are, not their sales. Keywords: GMV Max creators, list creators, creator identities, which creators, authorized creators, TikTok accounts for GMV Max, creator availability, identity list, creator profile link, tiktok profile url.

### `query({channel:"tiktok", resource:"gmv_max_post", mode:"list", params:{…}})`

List the TikTok posts (videos) available to a Product GMV Max campaign for a given TikTok Shop — the roster behind the "Posts" tab of Ads Manager GMV Max creative reporting. Each post returns item_id (post ID), text (caption), associated product spu_id_list, can_change_anchor, and creator identity_info (display_name + profile_image). Sort by GMV, post time, views, likes, CTR, or product clicks. Get store_id + store_authorized_bc_id from query({channel:"tiktok", resource:"gmv_max_store", mode:"list"}). IMPORTANT: TikTok returns an EMPTY list unless you scope creators — pass identity_list (from query({channel:"tiktok", resource:"gmv_max_creator", mode:"list"})) and/or set need_auth_code_video="true". For per-post GROSS REVENUE / performance, use insights({channel:"tiktok", lens:"gmv_max"}) grouped by item_id. Keywords: GMV Max posts, GMV Max videos, available posts, authorized videos, posts by creator, sort posts by GMV, top videos, which videos.

### `query({channel:"tiktok", resource:"catalog", mode:"list", params:{…}})`

List TikTok product catalogs in a Business Center. Catalogs power Dynamic Showcase Ads (DSA).

### `query({channel:"tiktok", resource:"catalog_product", mode:"list", params:{…}})`

List products inside a TikTok catalog.

### `query({channel:"tiktok", resource:"pixel", mode:"list", params:{…}})`

List TikTok pixels for the advertiser. Used for web conversion tracking + Events API.

### `query({channel:"tiktok", resource:"pixel_event", mode:"list", params:{…}})`

List the events configured on a TikTok pixel (Purchase, AddToCart, etc.) and their match-rate stats.

### `query({channel:"tiktok", resource:"saved_audience", mode:"list", params:{…}})`

List TikTok saved audiences (targeting templates) for the advertiser.

### `query({channel:"tiktok", resource:"comment", mode:"list", params:{…}})`

List comments on the advertiser's own TikTok video ads, scoped to one ad group over a time window. Filter by comment_status (ALL/HIDDEN/PUBLIC) and comment_type (ALL/COMMENT/REPLY). Use to monitor reactions and reply.

### `query({channel:"tiktok", resource:"lead_form", mode:"list", params:{…}})`

List TikTok Instant Forms (Lead Generation forms) attached to the advertiser. No CRUD — forms are created in TikTok Ads Manager UI.

### `query({channel:"tiktok", resource:"lead_submission", mode:"list", params:{…}})`

Fetch submissions for a TikTok Instant Form (Lead Gen). Production deployments should subscribe to the LEAD webhook for real-time delivery.

### `query({channel:"tiktok", resource:"lead_submission", mode:"get", params:{…}})`

Generate a CSV download URL for all submissions of a TikTok Instant Form within a time range.

### `query({channel:"tiktok", resource:"tcm_creator", mode:"list", params:{…}})`

Search creators in the TikTok Creator Marketplace (TTCM) by keyword, follower range, age, gender, location, category. Used to find influencers for branded content campaigns.

### `query({channel:"tiktok", resource:"tcm_order", mode:"list", params:{…}})`

List TTCM influencer collaboration orders / contracts for a TikTok advertiser.

### `query({channel:"tiktok", resource:"ad_media", mode:"get", params:{…}})`

Fetch downloadable creative media (video/image URLs) behind TikTok ads and posts. Pass EXACTLY ONE of ad_id, campaign_id, or post — it selects the mode: (1) ad_id — media URLs behind a regular TikTok ad: works for ads built from uploaded library videos (preview_url, valid ~6 hours), Spark Ads built from TikTok posts (resolved via identity; URL valid ~1 hour; photo posts return carousel image URLs), and image/carousel ads (temporary image URLs). Returns { ad_id, ad_format, media_source, assets: [{ type, source, download_url, download_url_expires_in, cover_url, width, height, duration_seconds, unavailable_reason }] }. Spark posts owned by TikTok-Shop (TTS_TT) identities cannot be resolved this way — use campaign_id for GMV Max videos. (2) campaign_id — downloadable VIDEO URLs for the posts a GMV Max campaign is running: resolves delivered posts via the GMV Max creative report, then fetches each post's temporary video URL. Each post returns item_id, caption, creator_display_name, video_id, download_url (signed, ~6 HOURS), cover_url (~24h), width/height/duration, plus cost / gross_revenue / creative_delivery_status. Defaults to the last 365 days; narrow with start_date/end_date, or pass item_ids to resolve specific posts directly. Removed/private/rejected/unauthorized posts come back with download_url null + an unavailable_reason. (3) post — download the video of any PUBLIC TikTok post (post ID or tiktok.com URL) and get back a STABLE re-hosted download URL (no expiry, shareable): returns { download_url, post_url, author_handle, caption, duration_seconds, width, height, size_bytes, cover_url, cached }. Results are cached per post. This is the reliable FALLBACK when ad_id/campaign_id modes return download_url null with an unavailable_reason — feed them the item_id or post_url from those responses. Only public posts; one post per call. IMPORTANT: ad_id/campaign_id download_url values are TEMPORARY signed links — download immediately, never store them; only the post mode returns a stable URL. Keywords: download TikTok ad video, ad video download link, get ad creative file, export TikTok creative, save ad video, spark ad video url, GMV Max video download, TikTok video downloader, creator post mp4, download video from TikTok link, ad media, creative download.

### `query({channel:"meta", resource:"instagram_account", mode:"get", params:{…}})`

Get Instagram Business account profile info — your OWN connected account, or PUBLIC discovery of ANY Business/Creator account by handle. OMIT username to read the connected account identified by ig_account_id: absolute current totals — follower count, follows count (accounts followed), media count (total posts), username, display name, biography, profile picture URL, and website. Use for "how many followers do we have", "what is my total follower count", "show me my IG profile info". PASS username to run Business Discovery on that PUBLIC handle instead — followers, name, bio, website, profile pic, and optionally its recent posts with public engagement (like_count, comments_count) via include_media + media_limit. Use for competitor research, influencer vetting, brand monitoring, "how many followers does {brand} have", "show me {competitor}'s recent posts", "look up @handle". ig_account_id is ALWAYS one of YOUR OWN connected IG Business account IDs (Meta requires a caller identity for discovery) — get it from accounts({channel:"meta", action:"list"}).default_instagram_id or the instagram_account list route. LIMITATIONS: discovery only works for Business/Creator targets (not personal accounts); reach, impressions, and engagement rate are private to the account owner. For time-windowed engagement of your own account (reach, profile_views, follower delta), use the Instagram account insights route instead. Keywords: IG profile, follower count, instagram bio, public IG profile, competitor instagram, influencer profile, find info on @username.

### `query({channel:"meta", resource:"instagram_account", mode:"list", params:{…}})`

List Instagram business accounts you can act on. DEFAULT (no query, no all_pages): the IG accounts connected to the current ad account for advertising — the Meta-recommended source of the instagram_user_id (instagram actor) needed when promoting IG posts as ads or creating ad creatives. PASS query (IG username or display name, case-insensitive substring) and/or all_pages=true to instead search Instagram business accounts across ALL the Facebook Pages the user manages — use this to find an IG account ID by name/handle when it is not connected to the current ad account. Keywords: list instagram accounts, connected IG accounts, find IG account id, instagram_user_id, instagram actor id, search instagram accounts by name, which IG account should I use for ads.

### `query({channel:"meta", resource:"instagram_media", mode:"list", params:{…}})`

Instagram posts/media for a connected IG account — LIST recent published posts, GET one post in full detail, or LIST posts where the account was TAGGED by other users (UGC). DEFAULT (no media_id, no tagged): LIST the account's recent published posts (most recent first) — media ID, type, truncated caption preview (first line), permalink, like/comment counts, timestamp, and boost_eligibility_info. The heavy media_url/thumbnail_url CDN links are omitted from lists by default to stay fast — set include_media_urls=true to include them. PASS media_id to GET a SINGLE post in full detail — the FULL caption plus the media_url + thumbnail_url CDN links. Use the media IDs with create({channel:"meta", resource:"ad", mode:"single"})'s source_instagram_media_id field to promote IG posts as ads; check boost_eligibility_info to confirm a post is eligible. PASS tagged=true to LIST posts (photos, videos, reels, carousels) where YOUR connected IG account has been tagged by other users — UGC monitoring, influencer activation tracking, and brand-mention discovery. Tagged results include the tagging account's username but only public engagement metrics (no reach or impressions — you don't own those posts) and page via paging.next_cursor → pass it back as `after`. ig_account_id is the Instagram Business Account ID (NOT the page ID) — required except when media_id is provided. For only the single LATEST post plus its comments, use query({channel:"meta", resource:"instagram_media", mode:"get"}) instead. Keywords: list instagram posts, instagram media, get instagram post, IG post by id, promote IG post as ad, boost eligibility, reels, carousel, post caption, permalink, engagement counts, tagged posts, UGC, who tagged us, influencer posts mentioning us, creator mentions.

### `query({channel:"meta", resource:"page_post", mode:"list", params:{…}})`

List posts on a Facebook Page — published, or scheduled (queued). DEFAULT (scheduled omitted or false): list PUBLISHED posts — post IDs (format: pageId_postId), message text, creation time, permalink URL, and engagement counts (likes, reactions, comments, shares). Use this to review Page performance, find post IDs for reading comments (publish({channel:"meta", resource:"comment", action:"moderate"})), promoting posts as ads (object_story_id), or managing existing content. PASS scheduled=true to list SCHEDULED (unpublished) posts instead — post ID, message, scheduled publish time, and creation time. Use it to review what content is queued before it goes live, or to find post IDs for updating or deleting scheduled posts. Keywords: list facebook posts, page posts, scheduled posts, queued posts, unpublished posts, post ids, engagement counts, what is queued on our page.

### `query({channel:"meta", resource:"comment", mode:"list", params:{…}})`

Read the comments on Instagram media or Facebook Page posts. Set `surface`: "instagram" lists the comments on an Instagram media_id (post or reel); "page" lists the comments on a Facebook Page post_id (returns id, message, author, created time, like count, hidden status — page_id is required). ad_account_id is required on every call. Keywords: read Instagram comments, read Facebook Page comments, monitor comments, comment moderation triage, audience engagement. For comments on your LATEST IG post, query({channel:"meta", resource:"instagram_media", mode:"get"}) resolves account + media + comments in one hop. To find a media_id from scratch, use query({channel:"meta", resource:"instagram_media", mode:"list"}) (needs an ig_account_id from accounts({channel:"meta", action:"list"}).default_instagram_id or query({channel:"meta", resource:"instagram_account", mode:"list"})). To reply / hide / delete a comment, use the comment moderation route (publish, action "moderate").

### `query({channel:"meta", resource:"ad_rule", params:{…}})`

Read Meta AUTOMATED RULES (the Ad Rules Engine) — the rules that watch ad objects on a schedule and act automatically (auto-pause on high CPA / low ROAS, budget auto-scaling, dayparting, spend alerts, ad rotation, budget rebalancing). Set `action`: "list" (all rules on the account), "get" (one rule by rule_id — returns the full evaluation/execution/schedule specs; when a rule shows status HAS_ISSUES read disable_error_code to see why Meta disabled it), "history" (execution log for ONE rule — rule_id REQUIRED; Meta has no account-level history), "active" (rules currently active on specific objects — pass selected_ids), or "count_by_type" (rule counts grouped by type — optional statuses filter). Use this to audit what automation exists, check whether a rule fired, or see which rules govern a campaign / ad set / ad. UNITS: money thresholds in returned specs are in the currency BASE UNIT (cents for USD); "get" annotates them with a human-readable _usd sibling. ad_account_id is REQUIRED on every call (e.g. act_1234567890).

### `query({channel:"meta", resource:"value_rule_set", params:{…}})`

Read Meta VALUE RULES (value rule sets) — bid multipliers that pay more (or less) for specific audiences, attached at the ad-set level. Set `action`: "list" (all value rule sets on the account) or "get" (one by value_rule_set_id — returns its rules: adjust_sign INCREASE/DECREASE, adjust_value percent, and the criterias they match on: age, gender, location, placement, device platform, OS). Use this to audit which audiences get bid up or down — e.g. lifetime-value (LTV) bidding, high-value segments, best-converting locations or placements. ad_account_id is REQUIRED on every call (e.g. act_1234567890).


## `insights` — read · 27 routes

### `insights({channel:"meta", lens:"campaign", params:{…}})`

Get performance insights at the campaign level. Returns spend, impressions, Clicks (all), Link clicks, CTR, CPC (all), CPM, reach, frequency, purchases, leads, cost per purchase, cost per lead, and ROAS. The "clicks" field is Clicks (all) — includes link clicks, post reactions, comments, shares, and other interactions. The "link_clicks" field is Link clicks only — actual clicks to the destination URL. Can filter by a specific campaign ID (campaign_id) or get all campaigns. To COMPARE specific campaigns side by side, pass campaign_ids (an array of 2+ IDs) — each campaign's totals are returned together under a "comparison" key (decide which to scale, pause, or optimize). Defaults to the last 7 days. Also filterable by campaign NAME (name_contains / name_not_contains — case-insensitive, multiple terms OR) and by delivery (had_delivery returns only campaigns that spent/delivered in the window even if paused now; min_spend sets a floor). Top-level standard event counts ("purchases", "leads", "add_to_cart", "view_content", "initiate_checkout", "complete_registration", "search") are DEDUPED to match Ads Manager — they prefer the standard event count and fall back to MAX of the source-specific copies (Pixel vs onsite). DO NOT sum overlapping action_types (e.g. `purchase` + `offsite_conversion.fb_pixel_purchase`, or `lead` + `offsite_conversion.fb_pixel_lead` + `onsite_web_lead`) from the raw "actions" array — they double-count the same conversion when Pixel and Meta-hosted forms/Shop both fire for it. Use the top-level field. The raw "actions" array includes ALL action types for breakdowns by attribution source if needed. OPTIMIZED EVENT FIRST: the top-level fields count STANDARD events only. Before ranking campaigns or reallocating budget, read the ad sets' optimization_goal + promoted_object (via query({channel:"meta", resource:"adset", mode:"list"})) — if an ad set optimizes for a custom event or Custom Conversion (promoted_object.custom_event_str / custom_conversion_id), judge it on THAT event: pass include_actions=true and read the "offsite_conversion.custom.<custom_conversion_id>" rows in actions[], not the standard leads/purchases counts. ROAS = purchase_value / spend. If the date range includes today, data for today is partial. Campaign costs are driven by the auction: Total Value = Bid × Estimated Action Rate + Ad Quality. Higher CPM/CPC with strong ROAS is expected — Meta optimizes for marginal efficiency (cost of next conversion), not average. Don't manually reallocate budget away from high-CPM campaigns that have strong ROAS. For deeper context on auction mechanics and pacing, call knowledge({topic:"meta_ads"}).

### `insights({channel:"meta", lens:"adset", params:{…}})`

Get performance insights at the ad set level. Returns metrics broken down by ad set: spend, impressions, Clicks (all), Link clicks, CTR, CPC (all), CPM, reach, purchases, leads, and ROAS. The "clicks" field is Clicks (all); "link_clicks" is Link clicks to the destination URL. Can filter by campaign_id or adset_id. Defaults to the last 7 days. Also filterable by ad set NAME (name_contains / name_not_contains — case-insensitive, multiple terms OR) and by delivery (had_delivery returns only ad sets that spent/delivered in the window even if paused now; min_spend sets a floor). Top-level standard event counts ("purchases", "leads", "add_to_cart", "view_content", "initiate_checkout", "complete_registration", "search") are DEDUPED to match Ads Manager. DO NOT sum overlapping action_types (e.g. Pixel + onsite copies of the same event) from the raw "actions" array — they double-count. Use the top-level fields. OPTIMIZED EVENT FIRST: the top-level fields count STANDARD events only. ALWAYS read each ad set's optimization_goal + promoted_object (returned by query({channel:"meta", resource:"adset", mode:"list"}) / query({channel:"meta", resource:"adset", mode:"get"})) BEFORE ranking, comparing, or restructuring — an ad set optimizing for a custom event or Custom Conversion (promoted_object.custom_event_str / custom_conversion_id) must be judged on THAT event: pass include_actions=true and count its "offsite_conversion.custom.<custom_conversion_id>" rows in actions[]. Ranking such ad sets on standard leads/purchases misgrades them. ROAS = purchase_value / spend. If the date range includes today, data for today is partial. Ad set performance naturally fluctuates. Daily CPA variation of 20-30% is normal due to pacing and auction dynamics — evaluate over 7+ day windows. If an ad set is in learning phase (~50 optimization events needed), performance is volatile and not indicative of long-term results. Check ad set delivery status via query({channel:"meta", resource:"adset", mode:"get"}) before judging performance. For deeper analysis of Meta ads mechanics, call knowledge({topic:"meta_ads"}).

### `insights({channel:"meta", lens:"ad", params:{…}})`

Get performance insights at the individual ad level. Returns detailed metrics for each ad: spend, impressions, Clicks (all), Link clicks, CTR, CPC (all), CPM, reach, purchases, leads, and ROAS. The "clicks" field is Clicks (all); "link_clicks" is Link clicks to the destination URL. Can filter by campaign_id, adset_id, or ad_id. Defaults to the last 7 days. Also filterable by ad NAME (name_contains / name_not_contains — case-insensitive, multiple terms OR) and by delivery (had_delivery returns only ads that spent/delivered in the window even if paused now; min_spend sets a floor). Top-level standard event counts ("purchases", "leads", "add_to_cart", "view_content", "initiate_checkout", "complete_registration", "search") are DEDUPED to match Ads Manager. DO NOT sum overlapping action_types (e.g. Pixel + onsite copies of the same event) from the raw "actions" array — they double-count. Use the top-level fields. OPTIMIZED EVENT FIRST: the top-level fields count STANDARD events only. ALWAYS read the parent ad set's optimization_goal + promoted_object (via query({channel:"meta", resource:"adset", mode:"get"}) / query({channel:"meta", resource:"adset", mode:"list"})) BEFORE ranking ads, picking winners, or pausing losers — ads in an ad set that optimizes for a custom event or Custom Conversion (promoted_object.custom_event_str / custom_conversion_id) must be judged on THAT event: pass include_actions=true and count each ad's "offsite_conversion.custom.<custom_conversion_id>" rows in actions[]. Ranking such ads on standard leads/purchases misgrades them. ROAS = purchase_value / spend. If the date range includes today, data for today is partial. Ad relevance diagnostics (quality_ranking, engagement_rate_ranking, conversion_rate_ranking, for ads with 500+ impressions) are returned when you pull a SINGLE ad (pass ad_id) or request them via `fields` — broad multi-ad pulls omit them by default so the query stays light and does not trip Meta's "reduce the amount of data" limit. To diagnose one ad, call this with its ad_id. These diagnose WHY an ad underperforms but are not auction inputs. Low quality → improve creative. Low engagement → test new hooks. Low conversion → check landing page. All low → audience-creative mismatch. For the full diagnostic framework, call knowledge({topic:"meta_ads"}).

### `insights({channel:"meta", lens:"account", params:{…}})`

Get account-level performance insights. Returns aggregated metrics across all campaigns: spend, impressions, Clicks (all), Link clicks, CTR, CPC (all), CPM, reach, purchases, leads, and ROAS. The "clicks" field is Clicks (all); "link_clicks" is Link clicks to the destination URL. Useful for a high-level overview of ad account performance, including TOTAL ACCOUNT SPEND for a date range (account spend, total spend, how much did I spend, ad account spend). Top-level standard event counts ("purchases", "leads", "add_to_cart", "view_content", "initiate_checkout", "complete_registration", "search") are DEDUPED to match Ads Manager. DO NOT sum overlapping action_types (e.g. Pixel + onsite copies of the same event) from the raw "actions" array — they double-count. Use the top-level fields. ROAS = purchase_value / spend.

### `insights({channel:"meta", lens:"top_performers", params:{…}})`

Get the top-performing campaigns, ad sets, or ads ranked by a chosen metric. Fetches insights at the specified level and sorts by the given metric (spend, roas, purchases, leads, clicks, ctr, impressions, cost_per_purchase, cost_per_lead). Returns the top N results. OPTIMIZED EVENT FIRST: the rankable metrics count STANDARD events only. Before treating this ranking as "best/worst performers", read the ad sets' optimization_goal + promoted_object (via query({channel:"meta", resource:"adset", mode:"list"})) — entities optimizing for a custom event or Custom Conversion (promoted_object.custom_event_str / custom_conversion_id) must be re-ranked on that event (compact=false, count the "offsite_conversion.custom.<custom_conversion_id>" action_type), not on standard leads/purchases. Responses are compact by default — the raw `actions[]` / `action_values[]` / `cost_per_action_type[]` arrays are stripped because the deduped top-level metrics (purchases, purchase_value, roas, etc.) are the canonical interface and the raw arrays can blow the response size past the MCP token cap. Pass compact=false if you need the raw arrays (e.g. for an event type that doesn't have a top-level shortcut).

### `insights({channel:"meta", lens:"performance_by_copy", params:{…}})`

Break down Meta ad performance by primary text (body copy) and headline variants — the best tool for analyzing which ad copy, messaging, hooks, headlines, or text variations are working. Works for ANY ad whose creative has multiple text options via asset_feed_spec — this includes Dynamic Creative (DCO), Advantage+ creative, Placement Asset Customization (PAC), and standard non-DCO ads that ship with text rotation. Up to 5 primary texts and 5 headlines per ad. Returns per-variant spend, impressions, clicks, CTR, CPM, CPC, purchases, ROAS, and leads using Meta's body_asset and title_asset insights breakdowns. Every row also includes "optimization_result" — the count of the event each ad set is actually optimizing for per its optimization_goal + promoted_object (app installs for app campaigns, leads for lead campaigns, purchases for sales campaigns, add-to-carts when that's the goal — and for ad sets optimizing a Custom Conversion or custom pixel event, THAT event), with "optimization_result_metric" naming which event it counts. Judge copy on optimization_result, not proxy metrics. The summary lists the optimization_goals and optimization_result_metrics across the analyzed ad sets. For plain single-creative ads (one body + one headline via object_story_spec), attributes the ad's total metrics to its single text — no per-variant breakdown exists because there's only one variant. Each ad's creative_type is tagged "multi_variant" or "single" so callers can tell them apart. Use this when the user asks: "which copy is performing best", "break down performance by headline", "compare primary text variants", "which ad text has the highest CTR", "what messaging converts", "which hook works", or "analyze copy performance across active ads". Returns per-ad rows (with each variant's metrics) plus aggregated rankings across all analyzed ads: top copy by spend, top copy by CTR (filtered to variants with 500+ impressions so noisy low-volume rows don't dominate), and top copy by ROAS. Use the "view" param to control the response: "aggregated" (default) returns ONLY the cross-ad copy rankings (best for ad-set-level analysis — combine with adset_id to roll every ad's copy into one deduped breakdown, no per-ad noise), "ads" returns ONLY the per-ad rows, "both" returns everything (very large on big accounts — scope with campaign_id/adset_id first). The aggregated rankings default to the top 15 variants per list; raise or lower with variant_limit (1-500). Defaults to ACTIVE ads over the last 30 days — override with status_filter, date_preset, or date_from/date_to. Meta does NOT support combining body_asset + title_asset in one call, so this tool issues two parallel breakdown requests for multi-variant ads; expect a short extra latency on accounts with many such ads. Purchases, leads, and add-to-cart counts are DEDUPED to match Ads Manager (prefer standard event, fall back to MAX of Pixel vs onsite) — do not sum raw action_type rows. ROAS = purchase_value / spend. If the date range includes today, today's row is partial.

### `insights({channel:"meta", lens:"winners", params:{…}})`

Top delivering ads of the past 30 days by default, judged on each ad set's optimized event — the ONLY entities worth ingesting when building account context. Feed creative_ids to creative({action:"analyze", channel:"meta"}) and ad_ids to insights({channel:"meta", lens:"performance_by_copy"}). Returns GROUPS, one per optimization_goal, ordered by total spend (the account's real center of gravity first); within each group ads rank by cost-per-result on the promoted_object-derived optimization event. Grouping exists because cost-per-result is only comparable within one goal — a cheap link click is not "better" than a $30 lead — so focus on the top-spend groups and treat low-spend cheap-event groups (e.g. old traffic campaigns) as secondary. `limit` caps each group, not the total. Defaults to the last 30 days — if that pull is enough, stop there. If it looks thin, REWIND one 30-day window at a time with since/until (today Jun 30 → pull May 1-30 next, then Apr 1-30) until you have enough winners. On huge accounts prefer raising min_spend over pulling more windows. min_spend filters out noise (default $1). SIGNIFICANCE: each row carries low_confidence (true when spend < ~$100 or results < ~10) with a reason — a thin-sample ad can post a freak cost-per-result / 10x ROAS on noise, so proven ads are ranked ABOVE low_confidence ones within each group. Treat low_confidence:true rows as unproven (keep them only as format examples), and judge real winners by cost-per-result vs the account's same-goal average, not raw ROAS on tiny spend.

### `insights({channel:"meta", mode:"job_create", lens:"job", params:{…}})`

Start an async insights job for heavy queries that would time out synchronously. Requires the data-warehousing add-on (Brand or Agency plan) — returns a 403 ENTERPRISE_ONLY error for orgs without it. Use when: (1) you need ad-level insights with 90+ day ranges, (2) any query with multiple breakdowns on a 1000+ ad account, or (3) a prior sync call returned "please reduce the amount of data" error. Returns a job_id. Poll with insights({channel:"meta", mode:"job_check", lens:"job"}) every ~15 seconds. Estimated completion is returned up front; typical jobs complete in 2-8 minutes. The job result is cached for 24 hours — safe to call once and check back later.

### `insights({channel:"meta", mode:"job_check", lens:"job", params:{…}})`

Check the status of an async insights job started with insights({channel:"meta", mode:"job_create", lens:"job"}). Returns { status: "running" | "complete" | "failed", percent?, rows? }. When complete, the "rows" array contains the full result set. Poll every 15-30 seconds — most jobs finish in 2-8 minutes.

### `insights({channel:"meta", lens:"instagram_account", params:{…}})`

Get ACCOUNT-LEVEL performance insights for an Instagram Business account — metrics for the whole account, NOT for individual posts. Returns time-series metrics (reach, follower_count) and total-value metrics (accounts_engaged, total_interactions, profile_views, likes, comments, shares, saves, replies). Use for questions like "how is my Instagram account performing this week", "how many new followers did I get", "what is my account-level reach", or any prompt asking about the IG account as a whole rather than specific posts. For per-post / per-reel / per-story metrics, use insights({channel:"meta", lens:"instagram_media"}) instead. IMPORTANT: Response format is an array of { name, period, values: [{ value, end_time? }] } objects per metric — NOT flat key-value pairs. Read a metric by finding the object with the matching name and accessing values[0].value. Period defaults to "day" (last 24 hours). Pass period="week" or period="days_28" for longer windows.

### `insights({channel:"meta", lens:"instagram_media", params:{…}})`

Get performance insights for a specific Instagram media post. IMPORTANT: Response format differs from ad insights — returns an array of { name, values: [{ value }] } objects, NOT flat key-value pairs. To read a metric, find the object with the matching name and access values[0].value. Metrics vary by media type: Reels return reach, likes, comments, saves, shares, and average watch time; Stories return reach, shares, follows, navigation, and replies; Feed posts return reach, likes, comments, saves, shares, follows, and profile activity.

### `insights({channel:"meta", lens:"ad_integrity", params:{…}})`

Run an integrity check on all active ads in the account. Requires the data-warehousing add-on (Brand or Agency plan), and only works on accounts that have been opted into daily sync. BEFORE CALLING: check `accounts({channel:"meta", action:"list"})` — each account has `warehouse_enabled: true|false`. Only call this on accounts where warehouse_enabled is true. If you call it on a non-enabled account, the response body will be `{ available: false, reason, message, alternatives }` — do not retry, just tell the user the account is not enabled for integrity checks. Finds: ads linking to domains not matching the business website or not approved, ads using a Facebook page or Instagram account that has not been used before, and ads with no destination URL. Designed for scheduled daily runs — safe to call once per day from an agent cron. Returns a list of anomalies with flag types (unknown_domain, unknown_page, unknown_instagram_account, no_destination_url). Reads from the local snapshot that the daily bulk pull populates. If the snapshot is stale (no recent sync), ask the user to check account_sync_state.

### `insights({channel:"google", lens:"campaign", params:{…}})`

Get performance insights at the campaign level from Google Ads. Returns metrics like spend, impressions, clicks, CTR, CPC, CPM, conversions, cost per conversion, and conversion rate. Can filter by a specific campaign ID or get all campaigns. Defaults to the last 7 days. Note: ctr, cpc, conversion_rate, and cost_per_conversion are computed server-side. Response includes: conversions (primary), conversions_value (revenue), all_conversions (all types incl. cross-device), all_conversions_value, and view_through_conversions. To get per-conversion-action breakdown (e.g., cost per lead vs cost per purchase), use breakdowns="conversion_action" — each row will include conversion_action_name and conversion_action_category. Conversion lag: Google attributes conversions to the click date, so conversion/ROAS/CPA figures for the last ~7 days are incomplete and keep rising over the following days — do not make pause/scale decisions on them (cost, clicks, impressions are unaffected).

### `insights({channel:"google", lens:"ad_group", params:{…}})`

Get performance insights at the ad group level from Google Ads. Returns metrics broken down by ad group. Can filter by campaign_id or ad_group_id. Defaults to the last 7 days. Note: ctr, cpc, conversion_rate, and cost_per_conversion are computed server-side. Response includes: conversions (primary), conversions_value (revenue), all_conversions (all types incl. cross-device), all_conversions_value, and view_through_conversions. To get per-conversion-action breakdown (e.g., cost per lead vs cost per purchase), use breakdowns="conversion_action" — each row will include conversion_action_name and conversion_action_category. Conversion lag: Google attributes conversions to the click date, so conversion/ROAS/CPA figures for the last ~7 days are incomplete and keep rising over the following days — do not make pause/scale decisions on them (cost, clicks, impressions are unaffected).

### `insights({channel:"google", lens:"ad", params:{…}})`

Get performance insights at the individual ad level from Google Ads. Returns detailed metrics for each ad. Can filter by campaign_id, ad_group_id, or ad_id. Defaults to the last 7 days. Note: ctr, cpc, conversion_rate, and cost_per_conversion are computed server-side. Response includes: conversions (primary), conversions_value (revenue), all_conversions (all types incl. cross-device), all_conversions_value, and view_through_conversions. To get per-conversion-action breakdown (e.g., cost per lead vs cost per purchase), use breakdowns="conversion_action" — each row will include conversion_action_name and conversion_action_category. Conversion lag: Google attributes conversions to the click date, so conversion/ROAS/CPA figures for the last ~7 days are incomplete and keep rising over the following days — do not make pause/scale decisions on them (cost, clicks, impressions are unaffected).

### `insights({channel:"google", lens:"quality_scores", params:{…}})`

Get keyword quality scores for an ad group. Returns quality score (1-10), creative quality, post-click experience quality, and expected CTR for each keyword.

### `insights({channel:"google", lens:"search_term_report", params:{…}})`

Get a search term report showing actual queries that triggered your Google Ads. Returns search terms with impressions, clicks, cost, conversions, and which campaign/ad group matched.

### `insights({channel:"tiktok", lens:"campaign", params:{…}})`

Get performance insights at the campaign level for standard (auction) TikTok Ads campaigns. Returns metrics like spend, impressions, clicks, CTR, CPC, CPM, conversions, and cost per conversion. Can filter by a specific campaign ID or get all campaigns. Defaults to the last 7 days. NOTE: This does NOT work for GMV Max campaigns — for GMV Max performance use insights({channel:"tiktok", lens:"gmv_max"}) instead.

### `insights({channel:"tiktok", lens:"ad_group", params:{…}})`

Get performance insights at the ad group level from TikTok Ads. Returns metrics broken down by ad group. Can filter by campaign_id or ad_group_id. Defaults to the last 7 days.

### `insights({channel:"tiktok", lens:"ad", params:{…}})`

Get performance insights at the individual ad level from TikTok Ads. Returns detailed metrics for each ad. Can filter by campaign_id, ad_group_id, or ad_id. Defaults to the last 7 days.

### `insights({channel:"tiktok", lens:"smart_plus_material", params:{…}})`

Get Smart+ creative-level material performance report (which video/asset is driving spend, impressions, conversions). report_type=overview (aggregate, default) or breakdown (time-series, requires start_date+end_date). dimensions is required.

### `insights({channel:"tiktok", lens:"gmv_max", params:{…}})`

Run a TikTok GMV Max campaign performance report (insights) — gross revenue, ROI, cost, orders, and (at the creative/post level) impressions, clicks, click rates, conversion rate, and video-view rates for GMV Max campaigns. This is the ONLY way to get GMV Max performance data; the standard insights({channel:"tiktok", lens:"campaign"}) does not work for GMV Max campaigns. BREAK DOWN BY CREATIVE/POST: set dimensions to "item_id" (item_id is the TikTok POST ID, i.e. the video/creative). NOTE: a creative-level (item_id) report REQUIRES both the campaign_ids and item_group_ids filters — TikTok rejects it otherwise. Get a campaign_id from query({channel:"tiktok", resource:"gmv_max_campaign", mode:"list"}) and item_group_id (product SPU) values from query({channel:"tiktok", resource:"gmv_max_post", mode:"list"}) (spu_id_list) or a product-level report. BREAK DOWN BY CREATOR: there is no standalone creator dimension, but every creative-level (item_id) row carries tt_account_name (the creator's DISPLAY NAME — a label like "mechanic", NOT the @handle) + tt_account_profile_image_url — request item_id with tt_account_name + gross_revenue/orders and group by tt_account_name to reproduce the Ads Manager "Creators" tab. IMPORTANT: tt_account_name and title are ATTRIBUTE fields that TikTok only returns when you scope to a SINGLE campaign_ids value AND a SINGLE item_group_ids value; pass multiple campaign or product IDs and they come back as "-" (the numeric metrics still aggregate correctly). To get a clickable creator profile_url (the real @handle), use query({channel:"tiktok", resource:"gmv_max_creator", mode:"list"}) (the report does not expose @handles). For the roster of available posts/creators incl. authorization status, use query({channel:"tiktok", resource:"gmv_max_post", mode:"list"}) and query({channel:"tiktok", resource:"gmv_max_creator", mode:"list"}). BREAK DOWN BY PRODUCT: set dimensions to "item_group_id" (the product SPU) AND include product_name (and optionally product_image_url) in metrics to get the human-readable product TITLE + thumbnail alongside cost/orders/gross_revenue/roi. Omit product_name and you only get the bare numeric SPU ID — so ALWAYS request product_name for a product breakdown. This works straight from the ads API; no TikTok Shop connection is required. A product-level (item_group_id) report REQUIRES a campaign_ids filter (TikTok rejects "item level" reports with no campaign ID — pass a campaign_id from query({channel:"tiktok", resource:"gmv_max_campaign", mode:"list"})); do NOT also put campaign_id in dimensions (product_name is an attribute metric incompatible with the campaign_id dimension). store_ids comes from query({channel:"tiktok", resource:"gmv_max_campaign", mode:"list"}) (each campaign includes its store_id) or query({channel:"tiktok", resource:"gmv_max_store", mode:"list"}). Keywords: GMV Max report, GMV Max insights, by creator, by creative, by post, by video, per-creator gross revenue, per-post performance, creative breakdown, top creators, top posts, which creator drove sales.

### `insights({channel:"tiktok", mode:"job_create", lens:"async_report", params:{…}})`

Create an async TikTok report task (for large/wide queries that would time out as sync). Poll with insights({channel:"tiktok", mode:"job_check", lens:"async_report"}); download with insights({channel:"tiktok", mode:"job_download", lens:"async_report"}).

### `insights({channel:"tiktok", mode:"job_check", lens:"async_report", params:{…}})`

Poll the status of an async TikTok report task. status: QUEUING, PROCESSING, SUCCESS, FAILED.

### `insights({channel:"tiktok", mode:"job_download", lens:"async_report", params:{…}})`

Get the download URL for a completed async TikTok report (CSV).

### `insights({channel:"tiktok", lens:"integrated", params:{…}})`

Get a synchronous TikTok integrated report with custom dimensions/metrics. Use when async overhead isn't needed.

### `insights({channel:"cross", lens:"cross_platform", params:{…}})`

Get cross-platform performance insights combining data from Meta, Google, and/or TikTok Ads into a unified view. Normalizes metrics across platforms for side-by-side comparison. Returns per-platform totals and a grand total. Requires appropriate platform headers. CONVERSION LAG: when platforms includes google and the window touches the last ~7 days, Google conversion metrics (conversions, conversions_value, ROAS, CPA) are systematically INCOMPLETE — Google attributes conversions to the click date and late conversions backfill for days. The default window (last 7 days) is exactly this zone, so a naive Meta-vs-Google comparison is biased against Google. The response prepends a ⚠️ note in that case — caveat the Google column or shift the window to end ≥7 days ago.


## `plan` — read · 11 routes

### `plan({channel:"meta", action:"ad_preview", params:{…}})`

Render a visual preview of an ad in a given placement format. Two modes: pass `ad_id` to preview an EXISTING ad, or omit ad_id and pass a `creative` spec to preview a DRAFT before publishing. Returns an HTML iframe snippet that renders the ad in a browser — AI agents cannot render this HTML directly, so use it only to hand a preview link to a human. For understanding an existing ad's content (text, headline, link, CTA), use query({channel:"meta", resource:"ad", mode:"get"}) instead.

### `plan({channel:"meta", action:"delivery_estimate", params:{…}})`

Get a delivery estimate for a targeting spec and optimization goal. Returns estimated daily reach, impressions, and clicks for the given audience and goal. Use this to validate audience size before creating an ad set. Note: delivery estimates may return zeros for very broad targeting or when Meta cannot model the specific optimization goal. If estimate_ready is false, the estimate is still being calculated. For audience sizing only, use plan({channel:"meta", action:"reach_estimate"}) instead — it is more reliable.

### `plan({channel:"meta", action:"reach_estimate", params:{…}})`

Get a reach estimate for a targeting spec. Returns the estimated number of people who could be reached with the given audience targeting. Use this to gauge audience size before creating an ad set.

### `plan({channel:"meta", action:"search_targeting", params:{…}})`

Search Meta detailed-targeting options by keyword. Set `type` to choose the category: "interests" (e.g. yoga, running shoes, B2B software), "behaviors" (e.g. frequent travelers, early tech adopters), "demographics" (income brackets, education levels, life events, parenting, relationship status, work industries), "education_schools", "education_majors" (fields of study), "work_employers" (specific companies), or "work_positions" (job titles). Returns matching options with id, name, and estimated audience_size — use the id in the corresponding ad set targeting array (targeting.interests / .behaviors / .demographics). For places/geo-targeting use plan({channel:"meta", action:"search_geo"}); to expand a few known interests into adjacent ones use plan({channel:"meta", action:"interest_suggestions"}).

### `plan({channel:"meta", action:"interest_suggestions", params:{…}})`

Given a list of seed interest names, return Meta-suggested related interests. Useful when the user names a few known interests and wants adjacent ones. Returns id, name, audience_size.

### `plan({channel:"meta", action:"search_geo", params:{…}})`

Search countries, regions, cities, zip codes, or designated market areas (DMAs) for geo-targeting. Returns id (a string key like "US" or "2421836"), name, audience_size. Use the id with type in targeting.geo_locations.

### `plan({channel:"tiktok", action:"search_hashtags", params:{…}})`

Search TikTok hashtags for targeting (hashtag-based behavioral targeting). Pass 1-10 keywords.

### `plan({channel:"tiktok", action:"search_geo", params:{…}})`

Search TikTok geo locations for targeting — countries, states, cities, ZIPs, DMAs. Returns location_ids.

### `plan({channel:"tiktok", action:"audience_size", params:{…}})`

Estimate TikTok audience reach for a targeting spec. Pass a full targeting object (locations, age, interests, etc.) to see the projected daily reach and impressions before launching.

### `plan({channel:"tiktok", action:"bid_recommendation", params:{…}})`

Get TikTok bid and budget recommendations before launching. TWO MODES, picked by which ids you pass: (1) STANDARD ad-group bid — pass objective_type + location_ids (omit store_id) to get a suggested bid for an ad group based on objective and target locations; the suggestion is only valid for Cost Cap bidding (bid_type=BID_TYPE_CUSTOM). (2) GMV MAX — pass store_id (TikTok Shop ID) + shopping_ads_type to get TikTok's recommended ROI target (ROAS) and daily budget for a GMV Max campaign on a TikTok Shop; use these values as starting points when creating a GMV Max campaign. Keywords: suggested bid, bid recommendation, recommended bid, cost cap bid, recommended ROAS, ROI target, recommended daily budget, GMV Max bid recommendation, what should I bid.

### `plan({channel:"tiktok", action:"search_targeting", params:{…}})`

Search or list TikTok targeting options for ad groups, selected by `type`: type=interest_keywords (default) — search TikTok interest keywords by `keyword`; returns interest_keyword_ids you can drop into an ad group's targeting. type=interest_categories — list TikTok interest categories (broader than keywords) for targeting. type=behaviors — list TikTok behavioral/action-category targeting categories (video-related and creator-related interactions). Keywords: interest targeting, search interests, interest keywords, interest categories, behavior targeting, behavioral categories, targeting options, who to target, audience interests.


## `creative` — read · 5 routes

### `creative({channel:"meta", action:"analyze", params:{…}})`

Analyze a Meta ad creative (video or image) AND explain it against real delivery data. Returns ~30 structured fields — creative content, hook breakdown, production fingerprint, timing markers (videos), psychology (awareness level, life-force-8, mindstate × regulatory focus, dominant emotion), conversion architecture — PLUS:
1. retention_analysis (videos, by ad_id): pulls hook rate, hold rate, and the 25/50/75/100% drop-off curve and maps WHERE viewers leave to WHAT is on screen, with a concrete retention fix. Explains why people scroll past or stop watching.
2. A KPI-anchored verdict: reads what the ad set OPTIMIZES for (leads, purchases, …) and judges the ad on its cost-per-result vs the account average — not on completion rate or ROAS.
3. interpretation: how to read the result + what to tell the user, plus factual callouts (missing CTA, no captions, weak product visibility, etc.).
Accepts ad_id (preferred — unlocks retention + KPI) or creative_id. Carousels analyze one card via card_index. MULTI-MEDIA ADS (Meta "multi-media": up to 10 images+videos in ONE ad): each call analyzes ONE asset, scored on its OWN delivery (per-media spend/CTR/CPA via the video_asset breakdown) — never the ad`s blended numbers. Defaults to the primary asset; pass media_index (0-based) to analyze another. The response`s multi_media block lists every asset (media_total + media_roster) so you can loop them, and carries ad_level_retention — the ad`s blended 25–100% curve, which Meta does NOT break out per media (only 3-sec plays do). To compare the assets of one multi-media ad, call once per media_index and diff the per-media verdicts.
Use when the user asks: analyze this ad, what makes this creative work, why are people scrolling past, where do viewers drop off, why is this ad winning/losing, should I scale this creative.

FOLLOW the interpretation block in the response: with several winning ads analyzed, diff their structured fields to surface the shared "winning DNA"; for new ideas, hold that DNA constant and vary one element at a time with real crafted copy.

Bulk-analysis guidance: Before fanning this tool out across many creatives in
an account, sort by spend descending and filter to roughly the top 90% of
cumulative spend (the Pareto set). Analyzing low-spend ads ($5–$20 lifetime)
wastes inference cost and produces non-actionable diagnoses because volume is
below the confidence floor (5,000 impressions / $30 spend). For a 200-ad
account this typically reduces analyses to 20–40 ads while covering 90%+ of
the account's actual ROI surface.

Can't see: DPA/Catalog ads (no single creative → UNSUPPORTED_ASSET_TYPE), comment sentiment, or audience saturation root cause. For bucket-style "is it fatigued / kill or scale" diagnosis, use creative({channel:"meta", action:"diagnose"}).

To turn this analysis into actual variations or net-new concepts, call knowledge({topic:"creative_frameworks"}) (the expert creative brain — hooks, scripting, copywriting, metadeception, open-loop retention) and apply it to craft real, original creative. Never delegatethe writing to platform text generation. Persist durable conclusions (winning DNA shared across winners, copy angles that convert) to account memory via update({resource:"account_brief"}), category creative or copy.

### `creative({channel:"google", action:"analyze", params:{…}})`

Analyze a Google Ads asset (image or YouTube video) and return ~30 structured fields. Accepts an asset resource_name (customers/{cust}/assets/{id}). YouTube videos are ingested natively by Gemini (no byte download). Image assets are downloaded and cached. Same field schema as creative({channel:"meta", action:"analyze"}).

Bulk-analysis guidance: Before fanning this tool out across many creatives in
an account, sort by spend descending and filter to roughly the top 90% of
cumulative spend (the Pareto set). Analyzing low-spend ads ($5–$20 lifetime)
wastes inference cost and produces non-actionable diagnoses because volume is
below the confidence floor (5,000 impressions / $30 spend). For a 200-ad
account this typically reduces analyses to 20–40 ads while covering 90%+ of
the account's actual ROI surface.

What this analysis can't see:
- DPA / Catalog ads don't have a single analyzable creative — performance is
  feed-driven. This tool returns UNSUPPORTED_ASSET_TYPE for those.
- A/B variant comparison: describes a single creative; does not compare
  variants. Call the tool on each ad and diff the structured fields.
- Comment sentiment, share patterns, save behavior: not included.
- Frequency, fatigue, or delivery signals: not included. Use the corresponding
  {platform}_diagnose_ad_creative tool for those (ships separately).

To turn this analysis into actual variations or net-new concepts, call knowledge({topic:"creative_frameworks"}) (the expert creative brain — hooks, scripting, copywriting, metadeception, open-loop retention) and apply it to craft real, original creative. Never delegatethe writing to platform text generation. Persist durable conclusions (winning DNA shared across winners, copy angles that convert) to account memory via update({resource:"account_brief"}), category creative or copy.

### `creative({channel:"tiktok", action:"analyze", params:{…}})`

Analyze a TikTok ad video and return ~30 structured fields. Accepts either ad_id or video_id. TikTok single-image creatives are not supported in v1 (returns UNSUPPORTED_ASSET_TYPE). Same field schema as creative({channel:"meta", action:"analyze"}).

Bulk-analysis guidance: Before fanning this tool out across many creatives in
an account, sort by spend descending and filter to roughly the top 90% of
cumulative spend (the Pareto set). Analyzing low-spend ads ($5–$20 lifetime)
wastes inference cost and produces non-actionable diagnoses because volume is
below the confidence floor (5,000 impressions / $30 spend). For a 200-ad
account this typically reduces analyses to 20–40 ads while covering 90%+ of
the account's actual ROI surface.

What this analysis can't see:
- DPA / Catalog ads don't have a single analyzable creative — performance is
  feed-driven. This tool returns UNSUPPORTED_ASSET_TYPE for those.
- A/B variant comparison: describes a single creative; does not compare
  variants. Call the tool on each ad and diff the structured fields.
- Comment sentiment, share patterns, save behavior: not included.
- Frequency, fatigue, or delivery signals: not included. Use the corresponding
  {platform}_diagnose_ad_creative tool for those (ships separately).

To turn this analysis into actual variations or net-new concepts, call knowledge({topic:"creative_frameworks"}) (the expert creative brain — hooks, scripting, copywriting, metadeception, open-loop retention) and apply it to craft real, original creative. Never delegatethe writing to platform text generation. Persist durable conclusions (winning DNA shared across winners, copy angles that convert) to account memory via update({resource:"account_brief"}), category creative or copy.

### `creative({action:"analyze_url", params:{…}})`

Analyze any image or video URL — Meta, Google, TikTok, or arbitrary third-party. Use this when you already have a direct asset URL and want to skip platform API lookup. Returns the same ~30-field schema as the platform-specific analyze tools. URL is SSRF-validated (no localhost, no private IPs, no file://).

Bulk-analysis guidance: Before fanning this tool out across many creatives in
an account, sort by spend descending and filter to roughly the top 90% of
cumulative spend (the Pareto set). Analyzing low-spend ads ($5–$20 lifetime)
wastes inference cost and produces non-actionable diagnoses because volume is
below the confidence floor (5,000 impressions / $30 spend). For a 200-ad
account this typically reduces analyses to 20–40 ads while covering 90%+ of
the account's actual ROI surface.

What this analysis can't see:
- DPA / Catalog ads don't have a single analyzable creative — performance is
  feed-driven. This tool returns UNSUPPORTED_ASSET_TYPE for those.
- A/B variant comparison: describes a single creative; does not compare
  variants. Call the tool on each ad and diff the structured fields.
- Comment sentiment, share patterns, save behavior: not included.
- Frequency, fatigue, or delivery signals: not included. Use the corresponding
  {platform}_diagnose_ad_creative tool for those (ships separately).

To turn this analysis into actual variations or net-new concepts, call knowledge({topic:"creative_frameworks"}) (the expert creative brain — hooks, scripting, copywriting, metadeception, open-loop retention) and apply it to craft real, original creative. Never delegatethe writing to platform text generation. Persist durable conclusions (winning DNA shared across winners, copy angles that convert) to account memory via update({resource:"account_brief"}), category creative or copy.

### `creative({channel:"meta", action:"diagnose", params:{…}})`

Diagnose WHY a Meta ad is failing or winning. Combines structured creative analysis (hook breakdown, awareness level, psychology, conversion architecture) with delivery signals (hook_rate, hold_rate, frequency, CPM, CTR, ROAS) and returns a bucket diagnosis: wrong_audience_or_fatigue | poor_hook | good_hook_poor_hold | good_creative_weak_cta | average_everything. Each bucket comes with a headline, fix_summary, and concrete fix_actions that reference specific creative fields. Auto-bootstraps the creative analyzer on cache miss — you do not need to call creative({channel:"meta", action:"analyze"}) first. Use this when the user asks: why is this ad failing, why is this creative working, is this ad fatigued, is the audience saturated, why is CTR low on this ad, should we kill or scale this ad, diagnose this creative, what should I fix on this ad.

Bulk-analysis guidance: Before fanning this tool out across many creatives in
an account, sort by spend descending and filter to roughly the top 90% of
cumulative spend (the Pareto set). Diagnosing low-spend ads ($5–$20 lifetime)
wastes compute and produces non-actionable diagnoses because volume is below
the confidence floor (5,000 impressions / $30 spend). For a 200-ad account
this typically reduces diagnoses to 20–40 ads while covering 90%+ of the
account's actual ROI surface.

What this diagnostic can't see:
- Audience saturation root cause (size, lookalike quality, market exhaustion).
  The wrong_audience_or_fatigue bucket flags the pattern but does not
  diagnose the underlying audience problem — use targeting tools for that.
- Attribution + multi-touch effects. ROAS is platform-attributed (single-touch,
  click-through dominant). Use insights({channel:"cross", lens:"cross_platform"}) or a server-side
  attribution layer for true incrementality.
- Below 1,000 impressions: returns confidence "low" (informational only —
  bucket thresholds aren't statistically stable at low volume). High-confidence
  diagnoses require ≥5,000 impressions and ≥$30 spend over the date range.
- DPA / Catalog ads: returns UNSUPPORTED_ASSET_TYPE (feed-driven, not
  creative-driven).

For the rewrite, pull the matching knowledge({topic:"creative_frameworks"}) topic: poor_hook -> "hooks";
good_hook_poor_hold -> "open_loops" + "scripting"; good_creative_weak_cta ->
"copywriting"; average_everything -> "core" + "hooks". Apply it with real crafted copy.


## `competitors` — read · 5 routes

### `competitors({channel:"meta", action:"brand_ads", params:{…}})`

See what Facebook & Instagram ads a competitor (or any company) is currently running, straight from Meta's public Ad Library. Give a company domain (e.g. nike.com) or a Facebook page URL and get their live ads: ad copy, headlines, CTAs, landing pages, images and videos, and run dates. Use this for competitor research, ad spying, creative inspiration / swipe files, and to see a brand's current Meta advertising strategy. Keywords: competitor ads, spy on competitor Facebook ads, what ads is a company running, Meta ad library, Instagram ads, competitive intelligence, ad creative research.

### `competitors({channel:"meta", action:"search", params:{…}})`

Search Meta's public Ad Library by keyword to discover ads running across many advertisers. Great for researching how a product category, offer, or angle is being advertised on Facebook & Instagram (e.g. search "webinar", "black friday", "meal kit"). Returns matching ads with copy, media, and advertiser info. Keywords: search Facebook ads by keyword, Meta ad library search, find ads by topic, category ad research, competitor angle research, ad inspiration.

### `competitors({channel:"google", action:"brand_ads", params:{…}})`

See what Google ads a competitor (or any company) is running, from the Google Ads Transparency Center. Give a company domain (e.g. clay.com) and get their ads across Search, Display, and video — with format, creative previews, the transparency-center link, and first/last-seen dates. Use for competitor research, ad spying, and understanding a brand's Google advertising. Keywords: competitor Google ads, Google ad transparency, spy on Google ads, what Google ads is a company running, search ads research, display ads.

### `competitors({channel:"tiktok", action:"search", params:{…}})`

Search TikTok's public ad library by keyword to discover ads and the advertisers running them. Returns matching ads with advertiser name, estimated audience, video and image creatives, and first/last-shown dates. Use each result's id with competitors({channel:"tiktok", action:"details"}) for full targeting and creative info. Great for TikTok competitor research and creative inspiration. Keywords: TikTok ad library, search TikTok ads, competitor TikTok ads, spy on TikTok ads, TikTok creative research, trending TikTok ads.

### `competitors({channel:"tiktok", action:"details", params:{…}})`

Get full details for a single TikTok ad by its id (from a competitors({channel:"tiktok", action:"search"}) result): the creative (video and images), advertiser info, and targeting breakdown (age, gender, location, audience size). Use this to deep-dive a specific competitor TikTok ad. Keywords: TikTok ad details, TikTok ad targeting, competitor TikTok creative, TikTok ad breakdown.


## `warehouse` — read · 2 routes

### `warehouse({action:"query", params:{…}})`

Query the local Xylo data warehouse — the daily snapshot of campaigns, ad sets, ads, daily insights, and URL history for the current ad account. Requires the data-warehousing add-on (Brand or Agency plan), and only for accounts that have been opted into daily sync. BEFORE CALLING: check `accounts({channel:"meta", action:"list"})` — each account has `warehouse_enabled: true|false` and `last_synced_at`. Only call this tool on accounts where warehouse_enabled is true. For other accounts (free plan, or add-on present but the account not sync-enabled), use live Meta tools (query({channel:"meta", resource:"campaign", mode:"list"}), query({channel:"meta", resource:"adset", mode:"list"}), query({channel:"meta", resource:"ad", mode:"list"}), insights({channel:"meta", lens:"ad"}), etc.) instead. If you call this on a non-enabled account, the response body will be `{ available: false, reason, message, alternatives }` instead of data — pivot to live Meta tools in that case. Much faster than live Meta calls when you need to slice/dice operational data (e.g. "top 20 ads by spend last 14 days", "every ad linking to domain X", "campaigns active but with zero spend today"). Supports filters (eq/in/gte/lte/gt/lt/like), ordering, limit up to 5000 rows, and offset-based pagination. Every query is automatically scoped to the current ad account. For the list of tables and columns, call warehouse({action:"schema"}) first. INSIGHTS_DAILY SCHEMA: all scalar metrics are top-level typed columns — filter and sort them natively. Common columns: level, entity_id, date_start, campaign_id, adset_id, ad_name, spend, impressions, clicks, ctr, cpc, cpm, reach, purchases, purchase_value, leads, roas, cost_per_purchase, cost_per_lead, link_clicks, landing_page_views. "entity_id" is the campaign/adset/ad id (depending on level). EXAMPLES: "top 50 ads by spend last 30d" → table: insights_daily, filters: [{level eq ad}, {date_start gte 2026-03-25}], order: {column: spend, desc: true}, limit: 50. "every ad in campaign X" → table: insights_daily, filters: [{level eq ad}, {campaign_id eq 123...}]. "every ad that delivered in the last 30 days (any status, including paused/deleted)" → table: insights_daily, filters: [{level eq ad}, {date_start gte 2026-03-25}, {spend gt 0}] — `insights_daily` has rows for every ad that ran, regardless of current status. To get creative metadata (page_id, destination_urls, creative_id) for those ads, query ad_snapshots — it now retains rows for any ad that delivered within the last 90 days, with still_operational=true|false telling you whether the ad is currently active. The `metrics` JSONB column holds the long-tail (full actions array with every action_type, action_values array, breakdown_values). It is EXCLUDED from default select because it is large (~2-5KB/row). Opt in with select including "metrics" only when you actually need raw action data.

### `warehouse({action:"schema", params:{…}})`

Get the schema (tables + key columns) of the local Xylo data warehouse (refreshed daily). Enterprise tier only, and only for warehouse-enabled ad accounts. Before calling, verify the current ad account has `warehouse_enabled: true` in the accounts({channel:"meta", action:"list"}) response. Returns `{ available: false, reason, message }` if the account is not enabled.


## `knowledge` — read · 5 routes

### `knowledge({topic:"creative_frameworks", params:{…}})`

Expert short-form ad-creative frameworks for GENERATING and IMPROVING creative — the companion to the analyze/diagnose tools. Call this when you need to: write ad variations, rewrite or build a hook, write a UGC / ad script, fix a salesy or "this is an ad"-feeling creative, raise watch-time / retention / hold rate, generate net-new ad concepts, sharpen ad copy, or make a creative feel native and scroll-stopping. Aggregates proven frameworks: the 8-step creative diagnostic funnel, the 3-step hook formula, viral scripting & storytelling, Harry-Dry copywriting, the Metadeception resistance system, and the Zeigarnik open-loop retention system, plus a 100-item hook template bank, checklists, worked examples, and a truthfulness guardrail. Call this AFTER creative({channel:"meta", action:"analyze"}) / creative({channel:"meta", action:"diagnose"}) (or the Google/TikTok/URL analyzers) when moving from "what the creative IS" to "make it better." DEFAULTS to the complete brain — pass specific topics only to scope down for bulk/cost-sensitive calls.

### `knowledge({topic:"creative_generation", params:{…}})`

Model-router + prompting guide for media({action:"generate"}) — read BEFORE picking a model. Covers per-model credits/aspect-ratios/reference-image support, the decision order for choosing gpt-image-edit / nano-banana-pro(-edit) / nano-banana(-edit) / gpt-image / flux-pro-ultra / flux-kontext / recraft-v3 (static product ads with text → gpt-image-edit), variants-vs-distinct-concepts semantics of num_images, and per-model prompting technique (art direction, lighting, text layout).

### `knowledge({topic:"video_generation", params:{…}})`

Video-model router + prompting guide for media({action:"generate_video"}) / edit_video — read BEFORE any video call. Covers kling-video vs seedance-video vs omni-video routing, per-second credit costs, the stills-first precision workflow (generate the exact frame as an image, then animate it), per-model prompting technique, and async job polling.

### `knowledge({topic:"account_research", params:{…}})`

The playbook for onboarding freshly connected accounts: group accounts into brands first (update({resource:"brand"})), then ingest one brand per conversation — website research, operational conventions, winners, copy insights, creative patterns, brand profile, reference assets. Read this before ingesting a new account or when a user asks Xylo to "learn", "set up", or "get up to speed on" their accounts or brands.

### `knowledge({topic:"meta_ads", params:{…}})`

Returns Meta Ads domain knowledge for interpreting ad performance data. Call this BEFORE analyzing performance issues, diagnosing underperformance, explaining metrics to users, or making optimization recommendations. Covers: auction mechanics (total value formula, marginal efficiency, the breakdown effect / high-CPM-strong-ROAS false alarm), learning phase (status interpretation, best practices), pacing (budget/bid pacing, why daily spend varies), bid strategies (how each strategy affects delivery and costs), auction overlap (self-competition detection), ad relevance diagnostics (quality/engagement/conversion ranking interpretation), performance fluctuations (normal vs. concerning patterns), strategy_lens (the standing account-strategy lens: creative-is-targeting/Andromeda, run-broad, question Meta suggestions, net-new-vs-variations, 7-day windows — call this when a user asks "should I narrow/scale my audience", is reacting to a one-day swing, or reports a rejection wave), and incremental_attribution (the incremental attribution model — call when a user asks about incrementality, true impact, or "which ads are actually driving sales"). Returns knowledge filtered by topic — pass specific topics for focused context.


## `create` — write · 34 routes

### `create({channel:"meta", resource:"ad", mode:"bulk", params:{…}})`

Bulk-create NET-NEW ads — many different ads in a single Meta Batch call. Different creatives, different ad sets, all submitted together. WHEN TO USE THIS: launching 3+ new ads at once. Most common cases: "12 ads across 5 ad sets," "4 creative variants in 3 different audiences," "10 different IG posts as ads." Each spec is independent and gets its own creative. Up to 500 ads per call. WHEN NOT TO USE: (a) Same creative fanning out across many ad sets → duplicate({channel:"meta", resource:"ads"}) is faster (no creative recreation). (b) Truly one-off ads (1-2 ads) → create({channel:"meta", resource:"ad", mode:"single"}) is fine. (c) Net-new creative that requires uploading from image_url at creation time → create({channel:"meta", resource:"ad", mode:"single"}) (it can upload). For bulk, push assets via the Xylo dashboard first. CREATIVE SHAPE PARITY: bulk supports the SAME creative shapes as create({channel:"meta", resource:"ad", mode:"single"}) — single image, single video, IG post promotion, FB post promotion, 5x headline / 5x primary_text rotation, feed+story placement customization (story_image_hash + image_hash, or story_video_id + video_id), carousels (child_attachments), creative_asset_groups_spec (Flexible Ad Format), and media_sourcing_spec (Multi-Media — up to 10 images+videos in one ad). The only thing it does NOT do is upload assets from URL. DYNAMIC CREATIVE (DCO) CREATION IS DEPRECATED — passing asset_feed_spec on a spec is rejected; use creative_asset_groups_spec (Flexible) instead. PRE-UPLOAD ASSETS: if the user has any new image/video that is not yet on Meta, tell them to open the Xylo dashboard, go to Media, and push the assets to Meta BEFORE calling this tool. Then they come back with the resulting image_hash and video_id values. (For existing IG/FB posts, just use source_instagram_media_id / object_story_id — no upload needed.) RECOMMENDED DEFAULT SHAPE: single-image-or-video ad with creative.story_image_hash (or story_video_id) + creative.bodies[5] + creative.headlines[5] + creative.instagram_actor_id — note that in bulk ALL text/media nests under each spec`s `creative` object (bodies/headlines), unlike single-mode create which takes top-level primary_texts/headlines. This is the same pattern Ads Manager emits for a modern PAC ad and is what should be generated unless the user explicitly asks for something else. Uses Meta`s Batch API with JSONPath result references — creative POST → ad POST chained server-side, 50 ops/batch (≈25 paired ads), parallel within each batch. ~10× faster than calling create({channel:"meta", resource:"ad", mode:"single"}) in a loop. RESPONSE: { summary: {requested, created, skipped_existing, failed}, created: [{spec_index, new_ad_id, creative_id}], skipped_existing: [{spec_index, existing_ad_id, name, adset_id}], errors: [{spec_index, stage, message}] }. summary is the source of truth for counts — report it to the user. Partial failures are expected; always inspect errors[]. New ads are PAUSED by default. SELF-HEALING / TIMEOUTS: ops that time out inside Meta`s batch are automatically verified against the target ad set and retried, then reported in created[] with verified_after_timeout or retried flags — those ads are REAL, do not recreate them. The call is also IDEMPOTENT BY AD NAME: if the tool call itself times out client-side, simply CALL IT AGAIN WITH THE EXACT SAME SPECS — ads that already exist are skipped (reported in skipped_existing with their ids) and only the missing ones are created, so a re-run always yields the full picture of what exists. Never rename specs when retrying after a timeout (renaming defeats the duplicate protection). NAME UNIQUENESS: ad names must be unique within an ad set — a spec whose name already exists in its target ad set is skipped, and two specs with the same name + adset_id in one call are rejected.

### `create({channel:"meta", resource:"adset", mode:"single", params:{…}})`

Create a new ad set within a campaign. An ad set defines the audience targeting, budget, schedule, and optimization goal. Created in PAUSED status by default. Required fields: name, billing_event (IMPRESSIONS, LINK_CLICKS, etc.), optimization_goal (REACH, LINK_CLICKS, OFFSITE_CONVERSIONS, etc.), and targeting. IMPORTANT: For conversion campaigns (OUTCOME_SALES objective, OFFSITE_CONVERSIONS optimization), you MUST include promoted_object with pixel_id and custom_event_type (e.g., PURCHASE). Use query({channel:"meta", resource:"pixel", mode:"list"}) to find the account's pixel ID. Always set bid_strategy to LOWEST_COST_WITHOUT_CAP unless told otherwise. ADVANTAGE+ AUDIENCE: if Meta rejects the create with subcode 1870227, it needs targeting.targeting_automation.advantage_audience — set it to 0 to keep your targeting strict (the usual choice for precise/agency targeting) or 1 to let Meta expand beyond it, then retry.

### `create({channel:"meta", resource:"custom_audience", mode:"single", params:{…}})`

Create a custom audience for targeting. Custom audiences let you target people who have already interacted with your business. Requires a name, type (e.g., WEBSITE, CUSTOMER_FILE, ENGAGEMENT), and a rule object defining the audience criteria.

### `create({channel:"meta", resource:"lookalike_audience", mode:"single", params:{…}})`

Create a lookalike audience based on an existing custom audience. Lookalike audiences find people similar to your existing audience. Requires a source audience, target country, and a ratio (0.01-0.20) where lower values mean closer similarity.

### `create({channel:"meta", resource:"campaign", mode:"single", params:{…}})`

Create a new Meta ad campaign. The campaign will default to PAUSED status for safety (it will not start spending immediately). You must specify a name and objective. Valid objectives: OUTCOME_AWARENESS, OUTCOME_ENGAGEMENT, OUTCOME_LEADS, OUTCOME_SALES, OUTCOME_TRAFFIC, OUTCOME_APP_PROMOTION. IMPORTANT: When using campaign budget optimization (CBO — setting daily_budget or lifetime_budget on the campaign), bid_strategy MUST be set here on the campaign, NOT on the ad set. Always set bid_strategy to LOWEST_COST_WITHOUT_CAP unless the user specifically requests a bid cap or cost cap.

### `create({channel:"meta", resource:"pixel", params:{…}})`

Create a new Meta Pixel for the ad account. Each ad account can only have one pixel. The pixel must be installed on your website to track visitor actions and conversions. Returns the new pixel ID.

### `create({channel:"google", resource:"ad_group", params:{…}})`

Create a new Google Ads ad group within a campaign. The ad group defaults to PAUSED status for safety. You must specify a name. Optionally set a CPC bid.

### `create({channel:"google", resource:"ad", params:{…}})`

Create a new Google Ads ad within an ad group. The ad defaults to PAUSED status for safety. You must provide a name, type, at least one headline, at least one description, and at least one final URL. Valid ad types: RESPONSIVE_SEARCH_AD, RESPONSIVE_DISPLAY_AD.

### `create({channel:"google", resource:"audience", params:{…}})`

Create a new Google Ads remarketing audience / user list for retargeting and customer match. For a website-visitor remarketing list (source RULE_BASED, the default) provide rule_url_contains — it matches people who visited a page whose URL contains that text. For a customer match list (source CRM) members are uploaded separately with update({channel:"google", resource:"user_list", action:"upload_members"}).

### `create({channel:"google", resource:"budget", params:{…}})`

Create a new standalone Google Ads campaign budget. The amount is in dollars (e.g., 50 = $50/day). Budgets can be shared across multiple campaigns.

### `create({channel:"google", resource:"campaign", params:{…}})`

Create a new Google Ads campaign. The campaign defaults to PAUSED status for safety. You must specify a name and campaign_type. Valid campaign types: SEARCH, DISPLAY, SHOPPING, PERFORMANCE_MAX, VIDEO.

### `create({channel:"google", resource:"conversion_action", params:{…}})`

Create a new Google Ads conversion action. You must specify a name and type. Valid types: WEBPAGE (website conversions), UPLOAD_CLICKS (offline click conversions), UPLOAD_CALLS (offline call conversions), AD_CALL (calls from ads), WEBSITE_CALL (calls from website). The type cannot be changed after creation.

### `create({channel:"google", resource:"experiment", params:{…}})`

Create and start a new Google Ads experiment (A/B test) for a campaign. This creates the experiment, sets up the control arm (the base campaign) and a treatment arm with the traffic split, then schedules (starts) it. Specify a campaign_id, name, traffic split, and optional date range.

### `create({channel:"google", resource:"keyword", params:{…}})`

Add one or more keywords to a Google Ads ad group. Each keyword requires text and match_type. Valid match types: BROAD, PHRASE, EXACT. You can add multiple keywords at once by providing an array. Keywords are created in ENABLED status by default.

### `create({channel:"google", resource:"asset_group", params:{…}})`

Create a new asset group for a Performance Max campaign. Requires a campaign_id, name, and at least one final URL. Defaults to PAUSED status.

### `create({channel:"google", resource:"user_list", params:{…}})`

Create a new Google Ads user list for customer match. Provide a name, optional description, and membership lifespan. After creation, upload members with update({channel:"google", resource:"user_list", action:"upload_members"}).

### `create({channel:"google", resource:"negative_keyword", params:{…}})`

Add negative keywords to a Google Ads campaign. Prevents ads from showing for these search terms. Supports bulk add — provide an array of keywords with text and match type.

### `create({channel:"google", resource:"negative_keyword_list", params:{…}})`

Create a new shared negative keyword list. After creating, add keywords to it and link it to campaigns.

### `create({channel:"tiktok", resource:"ad_group", mode:"single", params:{…}})`

Create a new TikTok Ads ad group within a campaign. You must specify a name and optimization_goal (CLICK, IMPRESSION, REACH, CONVERSION, INSTALL, VIDEO_VIEW, LEAD_GENERATION). The ad group defaults to DISABLE status for safety.

### `create({channel:"tiktok", resource:"campaign", mode:"single", params:{…}})`

Create a new TikTok Ads campaign. You must specify a name and objective. Valid objectives: REACH, TRAFFIC, VIDEO_VIEWS, LEAD_GENERATION, CONVERSIONS, APP_INSTALL, PRODUCT_SALES. The campaign defaults to DISABLE status for safety.

### `create({channel:"tiktok", resource:"smart_plus_campaign", mode:"single", params:{…}})`

Create a TikTok Smart+ (auto-optimized) campaign. Objectives: WEB_CONVERSIONS, APP_PROMOTION, LEAD_GENERATION.

### `create({channel:"tiktok", resource:"smart_plus_ad_group", mode:"single", params:{…}})`

Create a Smart+ ad group. Requires optimization_goal, promotion_type, and billing_event; targeting (locations, age, gender, languages, OS) is nested into the required targeting_spec automatically. schedule_type defaults to SCHEDULE_FROM_NOW.

### `create({channel:"tiktok", resource:"smart_plus_ad", mode:"single", params:{…}})`

Create a Smart+ ad. Requires ad_name. Creative is supplied via list-shaped fields: creative_list ([{creative_info}]), ad_text_list ([{ad_text}]), call_to_action_list ([{call_to_action}]), page_list ([{page_id}]), landing_page_url_list ([{landing_page_url}]). There is no flat "creatives" field.

### `create({channel:"tiktok", resource:"gmv_max_campaign", mode:"single", params:{…}})`

Create a TikTok GMV Max campaign (automated TikTok Shop campaign). Requires a TikTok Shop ID, the Business Center that owns the shop, an ROI target (roas_bid), and a daily budget. shopping_ads_type is PRODUCT or LIVE.

### `create({channel:"tiktok", resource:"catalog", mode:"single", params:{…}})`

Create a TikTok product catalog for Dynamic Showcase Ads (DSA). catalog_type: AUTO, DESTINATION, ECOM, ENTERTAINMENT, FLIGHT, HOTEL. region_code (e.g. US) and currency (e.g. USD) are required.

### `create({channel:"tiktok", resource:"catalog_product", mode:"bulk", params:{…}})`

Upload/upsert products into a TikTok catalog for Dynamic Showcase Ads from a hosted feed file. TikTok pulls products from a CSV/feed URL — inline product arrays are not supported. Pass file_url (the publicly reachable feed) and optional update_mode (INCREMENTAL default, or OVERWRITE).

### `create({channel:"tiktok", resource:"product_set", mode:"single", params:{…}})`

Create a Product Set inside a TikTok catalog (used to scope which products a DSA ad group promotes). Pass a filter spec.

### `create({channel:"tiktok", resource:"catalog_feed", mode:"single", params:{…}})`

Hook a TikTok catalog up to an auto-updating data feed (CSV/XML/JSON hosted at source_uri). TikTok pulls updates on the schedule you set. feed_name and update_mode are required.

### `create({channel:"tiktok", resource:"pixel", params:{…}})`

Create a new TikTok pixel for website conversion tracking.

### `create({channel:"tiktok", resource:"pixel_event", params:{…}})`

Register a custom event on a TikTok pixel (for example mapping a non-standard event_name to an optimization_event).

### `create({channel:"tiktok", resource:"ad", mode:"bulk", params:{…}})`

Bulk create up to ~20 TikTok ads inside one ad group in a single API call. Each creative entry typically has: ad_name, ad_format, identity_type, identity_id, video_id (or image_ids[]), ad_text, call_to_action, landing_page_url. Which fields are required depends on ad_format (TikTok enforces this server-side), so this schema stays permissive.

### `create({channel:"google", resource:"asset", params:{…}})`

Create a new Google Ads asset (ad extension) — sitelink, callout, structured snippet, call extension, or image — and optionally link it to a campaign in the same call. Provide the type and its type-specific fields: SITELINK: link_text + final_urls. CALLOUT: callout_text. STRUCTURED_SNIPPET: header + values. CALL: country_code + phone_number. IMAGE: name + image_data (base64). Pass campaign_id to create AND link the asset in one step — the link field_type is auto-determined from the asset type (IMAGE assets are automatically linked as AD_IMAGE). Omit campaign_id to just create the asset and link it to campaigns later.

### `create({channel:"meta", resource:"ad", mode:"single", params:{…}})`

Create a SINGLE Meta ad within an ad set — covers standard ads AND true Advantage+ Catalog Ads (Dynamic Product Ads / DPA). Requires adset_id + name + ad_account_id (e.g. act_1234567890). Created in PAUSED status by default. STANDARD AD (default path) — three creative modes: (1) PROMOTE INSTAGRAM POST: creative.source_instagram_media_id + instagram_actor_id + page_id (most campaigns also require call_to_action + link — check existing ads in the ad set to match the CTA pattern). (2) PROMOTE FACEBOOK POST: creative.object_story_id + page_id. (3) NEW CREATIVE: creative.page_id + headline + body + link + call_to_action + image_hash (or image_url or video_id). ALWAYS include creative.instagram_actor_id so the ad serves on Instagram placements. Provide up to 5 primary_texts and 5 headlines by default — Meta rotates them for performance. For placement-specific creatives set creative.story_image_hash alongside image_hash (feed vs story/reel). MULTIPLE MEDIA IN ONE AD: format="multi_media" — up to 10 images+videos (multi_media_images / multi_media_videos), Meta serves the best SINGLE asset per person/placement, works on ANY objective; format="flexible" — Meta MIXES media/texts and can render single image, video, or carousel (flexible_images / flexible_videos), OUTCOME_SALES / OUTCOME_APP_PROMOTION campaigns only. Dynamic Creative (DCO) creation is not supported — format="dynamic" / asset_feed_spec are rejected; use flexible instead. PRODUCT EXTENSIONS ("Add catalog items" under a static ad): set creative.product_extensions=true (+ creative.product_set_id to pin a set) — only when the user explicitly asks. CATALOG AD PATH: pass TOP-LEVEL product_set_id + page_id + link to create an Advantage+ Catalog Ad instead — the entire creative is rendered from the product catalog at delivery time (no static image or video); headline / primary_text / description are template strings with {{product.*}} placeholders (e.g. "{{product.name | titleize}}", "{{product.price}}"; Liquid filters supported); works both as one of many ads in a regular OUTCOME_SALES/TRAFFIC ad set and in a dedicated catalog-sales ad set (promoted_object.product_set_id). For 3+ ads with pre-uploaded assets prefer the bulk ad create route (~10x faster, same creative shapes); to fan an existing ad across ad sets use the ad duplicate route.

### `create({channel:"tiktok", resource:"audience", mode:"single", params:{…}})`

Create a TikTok custom audience for ad targeting or retargeting. Pick `type`: "customer_file" — build from a CRM list: pass id_type (EMAIL_SHA256 / PHONE_SHA256 / IDFA_SHA256 / GAID_SHA256) + identifiers[] (emails, phones, IDFAs, or GAIDs; Xylo SHA-256 hashes them automatically, set already_hashed=true to skip hashing). "engagement" — users who watched videos or visited the profile for a given identity (engagement_type + engagement_action, e.g. VIDEO_VIEW_2S / VIDEO_LIKE; identity_id or event_source_ids); great for retargeting organic engagement. "lookalike" — expand from a source/seed audience: source_audience_id + location_ids + lookalike_type (NARROW = most similar/smallest, BALANCED, BROAD = largest reach). "lead" — users who submitted one or more Lead Forms / Instant Forms (lead_form_ids). Every type needs `name`; pass advertiser_id to pick the TikTok ad account.


## `update` — write · 39 routes

### `update({resource:"account_brief", params:{…}})`

Save durable context ("memory" / account brief) for one ad account so the user never has to repeat it. CALL THIS WHENEVER the user reveals a lasting preference or fact about the account or its brand — for example: "always name campaigns like PROSPECTING_US_[date]", "new prospecting ad sets start at $50/day", "we mostly target US and Canada", "our brand voice is playful, no corporate speak", "put new ads in the Evergreen campaign", "our default product set is Bestsellers". Store ONE fact per `remember` entry with a short stable `key` in lowercase_snake_case (e.g. `naming_convention`, `default_daily_budget`, `primary_geo`, `audience_preferences`, `brand_voice`, `where_new_ads_go`), so re-saving the same key UPDATES that fact instead of creating a duplicate. Optionally set `category` to one of: naming, budgets, geo, audiences, targeting, product_sets, campaign_map, brand_voice, copy, creative, other (defaults to other). Use `forget` to drop keys that are no longer true. Store durable CONVENTIONS, not live state: "new prospecting ad sets start at $50/day" is a durable fact; the current budget of an existing campaign, which campaigns happen to be active today, or a time-limited promotion is NOT — read live state fresh each session, and re-verify any stored offer or promotion with the user before reusing it in new ads. `brief` SEEDS the account's freeform narrative brief (a short executive summary of the business and how the account runs): it is written only while the brief is empty (e.g. at first ingestion); once set it is user-owned and never overwritten — you get a warning instead, so fold updates into `remember` facts. Structured memory merges with whatever is already saved. You do not need to ask permission to save a fact the user clearly stated. Do NOT store secrets, access tokens, or personal data. Works for Meta, Google, and TikTok. Returns the updated brief + memory, plus `warnings` listing any entries that were skipped (bad key, empty/too-long value, or limits reached).

### `update({channel:"meta", resource:"ad", mode:"single", params:{…}})`

Update an existing ad. You can change the name, status, or creative fields. Only the fields you provide will be updated. FORMAT-PRESERVING: this tool will not change the creative shape. A single-text ad stays single-text; a rotation/asset_feed_spec ad stays a rotation ad; a Dynamic Creative ad stays Dynamic Creative. NARROW UPDATES (safe on asset_feed_spec creatives): pass `link` to change only the destination URL, or `primary_texts` / `headlines` to swap text rotations. When any of these three are provided alone (no other creative fields), the existing asset_feed_spec is preserved verbatim — the image, image_hash, CTA, and placement customization rules all stay intact, and only the specified field is swapped. CONSTRAINT: passing `primary_texts` / `headlines` arrays on an ad whose creative is NOT asset_feed_spec (a single-text ad) is REJECTED with an error — adding rotation arrays would convert the ad into Dynamic Creative. To add text rotation, create a NEW ad via create({channel:"meta", resource:"ad", mode:"single"}) (or create({channel:"meta", resource:"ad", mode:"bulk"})) with the arrays at creation time. To edit text on a single-text ad, use the singular `body` / `headline` fields inside `creative` instead. EXISTING-POST ADS CANNOT BE UPDATED: ads that promote an existing FB/IG post (object_story_id or source_instagram_media_id creatives, i.e. boosted posts) get their text, media, and link from the post itself — `primary_texts` / `headlines` / `link` swaps are REJECTED on them, even when query({channel:"meta", resource:"ad", mode:"get"}) shows a bodies array. FLEXIBLE ADS CANNOT BE TEXT-UPDATED: Flexible Ad Format ads keep their copy in creative_asset_groups_spec on the ad, not the creative — text swaps are REJECTED on them too; recreate with create({channel:"meta", resource:"ad", mode:"single"}) format="flexible" for new copy. CATALOG ADS (DPA / Advantage+ Catalog): supported, with constraints. Their copy is the catalog template`s message/headline — SINGLE strings (template params like {{product.name}} allowed), so pass one-element primary_texts / headlines arrays; multi-element arrays are rejected. CONSENT GATE: when query({channel:"meta", resource:"ad", mode:"get"}) shows multiple bodies on a catalog ad, those are Ads Manager AI-generated text options which the API cannot set or preserve — the first update attempt is REJECTED with an explanation. Relay it to the user; if they consent to permanently losing the AI options, retry with confirm_drop_ai_text_options=true. Skip existing-post and flexible ads when bulk-editing copy across an ad set; only `name`, `status`, and schedule changes work on them.

### `update({channel:"meta", resource:"ad", mode:"bulk", params:{…}})`

Update many ads at once — by explicit ID list or by filter. Two modes: (1) EXPLICIT: `ad_updates` with per-ad IDs + payloads. (2) FILTER: `filter` (e.g. {adset_id, name_contains, status}) + uniform `updates`. COMMON USE CASES: pause every ad in an ad set → filter={adset_id:"123"}, updates={status:"PAUSED"}. Pause all V1 ads → filter={name_contains:"V1"}, updates={status:"PAUSED"}. Swap creatives across many ads → ad_updates=[{ad_id, creative_id}, ...]. EFFICIENCY: 1 Meta read for filter resolution + parallel 50-op batch writes with omit_response_on_success. For 200 ads: 1 read + 4 batch writes = 5 HTTP calls. DRY RUN, LIMITS, PARTIAL FAILURES: same shape as update({channel:"meta", resource:"campaign", mode:"bulk"}).

### `update({channel:"meta", resource:"adset", mode:"single", params:{…}})`

Update an existing ad set. You can change name, status, budgets, targeting, bid strategy, or schedule. Only the fields you provide will be updated.

### `update({channel:"meta", resource:"adset", mode:"bulk", params:{…}})`

Update many ad sets at once — by explicit ID list or by filter. Two modes: (1) EXPLICIT: pass `adset_updates` with per-adset IDs + payloads. (2) FILTER: pass `filter` (e.g. {campaign_id, name_contains, status}) + uniform `updates`. COMMON USE CASES: pause every ad set in a campaign → filter={campaign_id:"123"}, updates={status:"PAUSED"}. Rename all V1 ad sets → filter={name_contains:"V1"}, updates={name:"..."}. IMMUTABLE FIELDS: is_dynamic_creative and destination_type cannot be changed after creation — Meta returns 200 but silently ignores them. They are not exposed by this tool. EFFICIENCY: 1 Meta read for the filter (id-only field projection) + parallel 50-op batch writes. For 200 ad sets: 1 read + 4 batch writes = 5 HTTP calls. DRY RUN, LIMITS, PARTIAL FAILURES: same shape as update({channel:"meta", resource:"campaign", mode:"bulk"}).

### `update({channel:"meta", resource:"audience", action:"users", params:{…}})`

Add or remove users in a CRM custom audience by uploading hashed customer data. Set operation="add" to add users, or operation="remove" to remove (suppress) them. Data is SHA-256 hashed server-side before sending to Meta. Up to 10,000 records per request. The schema array defines which fields each data row contains (e.g. ["EMAIL", "PHONE", "FN", "LN"]). Supported schema fields: EMAIL, PHONE, FN (first name), LN (last name), ZIP, CT (city), ST (state), COUNTRY, EXTERN_ID.

### `update({channel:"meta", resource:"campaign", mode:"single", params:{…}})`

Update an existing Meta ad campaign. You can change the name, status, budgets, or schedule. Only the fields you provide will be updated. SCHEDULING A LAUNCH: do NOT try to schedule a future launch with start_time + status=ACTIVE. Meta cannot move a campaign's start_time once its original start has passed, and setting status=ACTIVE takes it LIVE immediately (never at a future start_time). To launch at a specific future time, keep the campaign PAUSED and create a automation({channel:"meta", resource:"ad_rule"}) with rule_action UNPAUSE scheduled for that time — the reliable pattern. This tool reads the campaign back after any start_time change and returns a `warning` + applied_start_time/delivery_status if Meta ignored it.

### `update({channel:"meta", resource:"campaign", mode:"bulk", params:{…}})`

Update many campaigns at once — by explicit ID list or by filter. Two modes: (1) EXPLICIT: pass `campaign_updates` with per-campaign IDs and their individual update payloads. (2) FILTER: pass `filter` (e.g. {name_contains, status, objective}) plus `updates` — the same uniform update is applied to every matched campaign. COMMON USE CASES: pause all campaigns whose name contains "V1" → filter={name_contains:"V1"}, updates={status:"PAUSED"}. Bump every active campaign's daily budget to $100 → filter={status:["ACTIVE"]}, updates={daily_budget:100}. EFFICIENCY: filter mode does ONE Meta read (id-only field projection) then chunks writes into 50-op Meta Batch calls dispatched 3 in parallel, with omit_response_on_success=true to cut payload. For 200 campaigns: 1 read + 4 batch writes = 5 Meta HTTP calls total. DRY RUN: pass dry_run=true to preview which campaigns the filter matches without writing anything. LIMITS: default 20 campaigns per call, max 200. PARTIAL FAILURES: response includes {succeeded, succeeded_ids, retried_ids, failed:[{id,error_code,error_message,transient}]}. Always inspect failed[]. TRANSIENT FAILURES ARE AUTO-RETRIED: a write that times out inside Meta`s batch is automatically retried once after a settle (field updates are idempotent, so this is safe); ids that succeeded on the retry appear in BOTH succeeded_ids and retried_ids and need no further action. Anything left in failed[] survived both attempts — safe to re-run the call for just those ids (re-applying the same update twice is harmless).

### `update({resource:"brand_profile", params:{…}})`

Save or update the structured brand profile — the identity facts that make AI-generated creative (media({action:"generate"})) and account research on-brand instead of generic. The profile is brand-level, not per account: it is shared across every ad account attached to the same brand, so saving it once from any of those accounts keeps them all consistent. If this account is not yet attached to a brand, the first save here creates one automatically and attaches the account to it. CALL THIS WHENEVER the user states a lasting brand fact: the brand's website or key URLs, colors, fonts, the brand's one-line positioning, tone of voice, things the brand does/does not say, claims it is legally or factually banned from making, product facts, proof points (reviews, awards, guarantees), audience notes, or offer notes. MERGE SEMANTICS: `patch` is merged into the existing profile, not replaced wholesale — top-level keys you omit are left untouched. For any key you DO include: an array or scalar value REPLACES the existing value entirely (no appending); an explicit `null` DELETES that key. `voice` is the one exception — its own sub-keys (tone, descriptors, do, dont, banned_claims) merge individually the same way, so you can update just `voice.tone` without clobbering `voice.do`/`voice.dont`. Recognized top-level keys: `urls` (array of {url, label?} — the brand website, shop, key landing pages; save the site URL when you research a brand), `colors` (array of {hex, role?}), `fonts` (array of strings), `anchor_line` (one-line brand positioning), `voice` ({tone, descriptors[], do[], dont[], banned_claims[]}), `product_truths` (array of factual claims safe to make), `proof_points` (array of credibility signals — reviews, awards, guarantees), `audience_notes` (freeform), `offer_notes` (freeform). Unrecognized keys are stored as-is (forward-compatible) but only the keys above are read by generation and account research. Read the current profile via accounts({action:"read_brief"}) (returns it under `brand_profile`, alongside the `brand` this account is attached to). Works for Meta, Google, and TikTok.

### `update({resource:"brand", params:{…}})`

Group ad accounts into brands: attach accounts (Meta, Google, or TikTok) to a brand, creating the brand if needed. This is GROUPING ONLY — no brand content yet; the profile is written later with update({resource:"brand_profile"}) and it is shared by every account attached to the brand. Each assignment takes brand_id (an existing brand from accounts({action:"list_brands"})) OR brand_name (finds an existing brand by that exact name case-insensitively, otherwise creates it — so assigning several accounts to the same brand_name yields ONE brand). Up to 100 assignments per call; results are reported per assignment and one failure does not stop the rest. USE THIS as the FIRST step when onboarding an org with many ad accounts ("set up my brands", "group my accounts"): list accounts on every channel, propose the account→brand grouping to the user, confirm it, then assign everything in one call. Ingest brand content afterwards, one brand at a time (knowledge({topic:"account_research"})).

### `update({channel:"meta", resource:"pixel", params:{…}})`

Update pixel settings such as its name or automatic matching configuration. Automatic matching improves attribution by hashing and matching customer data (email, phone, etc.) sent with pixel events.

### `update({channel:"google", resource:"ad_group", params:{…}})`

Update an existing Google Ads ad group. You can change the name, status, or CPC bid. Only the fields you provide will be updated.

### `update({channel:"google", resource:"campaign_asset", action:"link", params:{…}})`

Link an existing asset to a Google Ads campaign. The asset must already exist — use create({channel:"google", resource:"asset"}) first, then link it. Or use create({channel:"google", resource:"asset"}) for both. Note: for IMAGE assets, use field_type AD_IMAGE (not IMAGE) when linking.

### `update({channel:"google", resource:"campaign_asset", action:"unlink", params:{…}})`

Remove an asset link from a Google Ads campaign. The asset itself is not deleted — only the association with the campaign is removed.

### `update({channel:"google", resource:"budget", params:{…}})`

Update an existing Google Ads campaign budget. You can change the name or amount. Changes apply to all campaigns using this budget.

### `update({channel:"google", resource:"campaign", params:{…}})`

Update an existing Google Ads campaign. You can change the name, status, budget, or schedule. Only the fields you provide will be updated.

### `update({channel:"google", resource:"conversion_action", params:{…}})`

Update an existing Google Ads conversion action. You can change name, status, category, counting type, lookback windows, and value settings. Type cannot be changed.

### `update({channel:"google", resource:"experiment", params:{…}})`

Update an existing Google Ads experiment. You can change the name, description, or dates. Only experiments in SETUP status can be fully edited.

### `update({channel:"google", resource:"experiment", action:"promote", params:{…}})`

Promote a Google Ads experiment to replace the original campaign. This makes the experiment arm the new primary campaign and removes the original. WARNING: This cannot be undone.

### `update({channel:"google", resource:"experiment", action:"end", params:{…}})`

End a running Google Ads experiment. This stops the experiment and reverts all traffic to the original campaign. Use this if the experiment is not performing well.

### `update({channel:"google", resource:"keyword", params:{…}})`

Update an existing keyword in a Google Ads ad group. You can change the status or CPC bid. Only the fields you provide will be updated.

### `update({channel:"google", resource:"asset_group", params:{…}})`

Update an existing Google Ads asset group. You can change the name, status, final URLs, or display URL paths.

### `update({channel:"google", resource:"asset_group_asset", action:"link", params:{…}})`

Link an existing asset to a Performance Max asset group. The asset must already exist. Use create({channel:"google", resource:"asset"}) to create one first, then link it here.

### `update({channel:"google", resource:"asset_group_asset", action:"unlink", params:{…}})`

Remove (unlink) an asset from a Performance Max asset group. The asset itself is not deleted — only the association with the asset group is removed. You must pass the same field_type the asset was linked under, since the same asset can be linked under multiple field types.

### `update({channel:"google", resource:"user_list", action:"upload_members", params:{…}})`

Upload members to a Google Ads user list for customer match. Members must be pre-hashed using SHA-256 (lowercase, trimmed). Returns the number of received operations.

### `update({channel:"google", resource:"user_list", action:"remove_members", params:{…}})`

Remove members from a Google Ads user list. Members must be identified using the same hashed format as they were uploaded with.

### `update({channel:"google", resource:"recommendation", action:"apply", params:{…}})`

Apply a Google Ads recommendation. This implements the suggested change (e.g., adding a keyword, adjusting a budget). The recommendation is permanently consumed after applying.

### `update({channel:"google", resource:"recommendation", action:"dismiss", params:{…}})`

Dismiss a Google Ads recommendation. This hides the recommendation from future queries. Dismissed recommendations may reappear if conditions change.

### `update({channel:"google", resource:"negative_keyword_list", action:"add_keywords", params:{…}})`

Add negative keywords to a shared negative keyword list. Supports bulk add.

### `update({channel:"google", resource:"negative_keyword_list", action:"remove_keyword", params:{…}})`

Remove a keyword from a shared negative keyword list.

### `update({channel:"google", resource:"negative_keyword_list", action:"link_to_campaign", params:{…}})`

Link a shared negative keyword list to a campaign. All keywords in the list will apply to the campaign.

### `update({channel:"tiktok", resource:"ad_group", mode:"single", params:{…}})`

Update an existing TikTok Ads ad group. You can change the name, status, budget, bid price, or schedule. Only the fields you provide will be updated.

### `update({channel:"tiktok", resource:"campaign", mode:"single", params:{…}})`

Update an existing TikTok Ads campaign. You can change the name, status, budget, or budget mode. Only the fields you provide will be updated.

### `update({channel:"tiktok", resource:"smart_plus_campaign", mode:"single", params:{…}})`

Update a TikTok Smart+ campaign — change name, status, or budget.

### `update({channel:"tiktok", resource:"gmv_max_campaign", mode:"single", params:{…}})`

Update a TikTok GMV Max campaign: change the ROI target (roas_bid) or the daily budget. To enable, disable, or delete the campaign, use status({channel:"tiktok", resource:"gmv_max_campaign"}) instead.

### `update({channel:"tiktok", resource:"audience", action:"append", params:{…}})`

Append more identifiers to an existing TikTok customer-file audience. Same hashing rules as creation.

### `update({channel:"tiktok", resource:"audience", action:"share", params:{…}})`

Share a TikTok custom audience with other advertiser accounts (e.g. across agency-managed accounts).

### `update({channel:"tiktok", resource:"campaign", mode:"bulk", params:{…}})`

Bulk update fields on many TikTok campaigns. Each entry needs campaign_id; settable fields include campaign_name, budget. Pass operation_status (ENABLE/DISABLE/DELETE) to also change status — it is routed to the dedicated status endpoint. Each campaign is updated individually; the response reports per-campaign success/failure.

### `update({channel:"tiktok", resource:"ad_group", mode:"bulk", params:{…}})`

Bulk update many TikTok ad groups (budgets, bids, schedules, targeting). Each entry needs adgroup_id. Pass operation_status (ENABLE/DISABLE/DELETE) to also change status — it is routed to the dedicated status endpoint. (budget_mode is not an updatable field and is ignored.) Each ad group is updated individually; the response reports per-ad-group success/failure.


## `status` — write · 14 routes

### `status({channel:"tiktok", resource:"gmv_max_campaign", params:{…}})`

Enable, disable (pause), or delete a TikTok GMV Max campaign.

### `status({channel:"tiktok", resource:"campaign", mode:"bulk", params:{…}})`

Bulk pause / enable / delete TikTok campaigns by ID. TikTok caps each status request at 20 IDs; larger lists are automatically chunked into batches of 20, and a failed chunk does not stop the rest. RESPONSE: { success, requested, updated, updated_ids, failed: [{id, error}] } — report the counts; anything in failed[] definitively did not change (safe to re-run for just those ids, status changes are idempotent).

### `status({channel:"tiktok", resource:"ad_group", mode:"bulk", params:{…}})`

Bulk pause / enable / delete TikTok ad groups by ID. TikTok caps each status request at 20 IDs; larger lists are automatically chunked into batches of 20, and a failed chunk does not stop the rest. RESPONSE: { success, requested, updated, updated_ids, failed: [{id, error}] } — report the counts; anything in failed[] definitively did not change (safe to re-run for just those ids, status changes are idempotent).

### `status({channel:"tiktok", resource:"ad", mode:"bulk", params:{…}})`

Bulk pause / enable / delete TikTok ads by ID. TikTok caps each status request at 20 IDs; larger lists are automatically chunked into batches of 20, and a failed chunk does not stop the rest. RESPONSE: { success, requested, updated, updated_ids, failed: [{id, error}] } — report the counts; anything in failed[] definitively did not change (safe to re-run for just those ids, status changes are idempotent).

### `status({channel:"meta", resource:"campaign", params:{…}})`

Pause or resume (activate) a Meta ad campaign. Set status=PAUSED to immediately stop ad delivery and spend for the campaign, or status=ACTIVE to reactivate ad delivery and spending on a paused campaign. Make sure the campaign has ad sets and ads before resuming. Requires the campaign_id and the ad account (ad_account_id, e.g. act_1234567890).

### `status({channel:"meta", resource:"adset", params:{…}})`

Pause or resume (activate) a Meta ad set. Set status=PAUSED to stop all ads in the ad set from delivering, or status=ACTIVE to reactivate ad delivery for all active ads in the ad set. Requires the adset_id and the ad account (ad_account_id, e.g. act_1234567890).

### `status({channel:"meta", resource:"ad", params:{…}})`

Pause or resume (activate) a Meta ad. Set status=PAUSED to stop the ad from being delivered, or status=ACTIVE to reactivate the ad for delivery. Requires the ad_id and the ad account (ad_account_id, e.g. act_1234567890).

### `status({channel:"google", resource:"campaign", params:{…}})`

Pause or resume (enable) a Google Ads campaign by setting its status. status=PAUSED immediately stops ad delivery and spend; status=ENABLED reactivates delivery and spending. Make sure the campaign has ad groups and ads before enabling it. Requires the campaign_id; pass customer_id to target a specific Google Ads account.

### `status({channel:"google", resource:"ad_group", params:{…}})`

Pause or resume (enable) a Google Ads ad group by setting its status. status=PAUSED stops all ads and keywords in the ad group from delivering; status=ENABLED reactivates delivery for all enabled ads and keywords in the ad group. Requires the ad_group_id; pass customer_id to target a specific Google Ads account.

### `status({channel:"google", resource:"ad", params:{…}})`

Set the status of a Google Ads ad: pause, resume (enable), or remove it. status=PAUSED immediately stops the ad from delivering; status=ENABLED reactivates delivery (make sure the parent ad group and campaign are also enabled); status=REMOVED permanently stops it. This is the ONLY update a Google Ads ad supports — per Google Ads policy, ad content (headlines, descriptions, URLs) is immutable; to change content, create a new ad and remove the old one. Requires ad_group_id + ad_id; pass customer_id to target a specific Google Ads account.

### `status({channel:"meta", resource:"campaign", mode:"bulk", params:{…}})`

Pause or resume MANY Meta campaigns in one call: pass campaign_ids + status ("PAUSED" stops delivery and spend immediately; "ACTIVE" resumes it). Runs on the batched bulk-update backend (chunked Meta Batch writes, auto-retry on transient failures). Response includes {succeeded, succeeded_ids, failed:[...]} — always inspect failed[]. Idempotent: re-applying the same status is harmless. Large activations require a confirm_token round trip.

### `status({channel:"meta", resource:"adset", mode:"bulk", params:{…}})`

Pause or resume MANY Meta ad sets in one call: pass adset_ids + status ("PAUSED" stops delivery and spend immediately; "ACTIVE" resumes it). Runs on the batched bulk-update backend (chunked Meta Batch writes, auto-retry on transient failures). Response includes {succeeded, succeeded_ids, failed:[...]} — always inspect failed[]. Idempotent: re-applying the same status is harmless. Large activations require a confirm_token round trip.

### `status({channel:"meta", resource:"ad", mode:"bulk", params:{…}})`

Pause or resume MANY Meta ads in one call: pass ad_ids + status ("PAUSED" stops delivery and spend immediately; "ACTIVE" resumes it). Runs on the batched bulk-update backend (chunked Meta Batch writes, auto-retry on transient failures). Response includes {succeeded, succeeded_ids, failed:[...]} — always inspect failed[]. Idempotent: re-applying the same status is harmless. Large activations require a confirm_token round trip.

### `status({channel:"tiktok", resource:"campaign", mode:"single", params:{…}})`

Pause or resume a TikTok Ads campaign by setting its status. status=DISABLE pauses the campaign — immediately stops ad delivery and spend. status=ENABLE resumes a paused campaign — reactivates ad delivery and spending (make sure the campaign has ad groups and ads before resuming). Requires the campaign_id. Keywords: pause TikTok campaign, resume TikTok campaign, stop campaign spend, turn campaign off, turn campaign on, enable campaign, disable campaign, reactivate campaign.


## `delete` — write · 17 routes

### `delete({channel:"meta", resource:"ad", params:{…}})`

Delete an ad. This sets the ad status to DELETED and stops delivery. WARNING: This cannot be easily undone.

### `delete({channel:"meta", resource:"adset", params:{…}})`

Delete an ad set. This sets the ad set status to DELETED and stops all ads within it. WARNING: This cannot be easily undone.

### `delete({channel:"meta", resource:"campaign", params:{…}})`

Delete a Meta ad campaign. This sets the campaign status to DELETED. All ad sets and ads under this campaign will also stop delivering. WARNING: This cannot be easily undone.

### `delete({resource:"brand_asset", params:{…}})`

Permanently delete a durable brand asset (saved via media({action:"save_brand_asset"})) from this account's brand. This does not affect ads already built with it — only removes it from future reference-image use and from accounts({action:"read_brief"}).

### `delete({channel:"google", resource:"ad", params:{…}})`

Delete a Google Ads ad. This sets the ad status to REMOVED and stops delivery permanently. WARNING: This cannot be undone. Consider pausing the ad instead if you might want it later.

### `delete({channel:"google", resource:"asset", params:{…}})`

Delete a Google Ads asset. This removes the asset and all its links. WARNING: Cannot be undone.

### `delete({channel:"google", resource:"audience", params:{…}})`

Delete a Google Ads remarketing audience. This closes the audience membership — no new users will be added. Existing members remain until their membership expires. WARNING: This cannot be undone.

### `delete({channel:"google", resource:"budget", params:{…}})`

Delete a Google Ads campaign budget. The budget must not be referenced by any campaigns. WARNING: This cannot be undone.

### `delete({channel:"google", resource:"campaign", params:{…}})`

Delete a Google Ads campaign. This sets the campaign status to REMOVED. All ad groups and ads under this campaign will also stop delivering. WARNING: This cannot be undone.

### `delete({channel:"google", resource:"conversion_action", params:{…}})`

Delete a Google Ads conversion action. This sets the status to REMOVED. Conversions will no longer be recorded for this action. WARNING: Cannot be undone.

### `delete({channel:"google", resource:"experiment", params:{…}})`

Delete a Google Ads experiment. This removes the experiment and its associated draft campaign. WARNING: This cannot be undone.

### `delete({channel:"google", resource:"keyword", params:{…}})`

Remove a keyword from a Google Ads ad group. This permanently deletes the keyword. WARNING: This cannot be undone. Consider pausing the keyword instead if you might want it later.

### `delete({channel:"google", resource:"asset_group", params:{…}})`

Delete a Google Ads asset group. This sets the status to REMOVED. WARNING: This cannot be undone.

### `delete({channel:"google", resource:"user_list", params:{…}})`

Close a Google Ads user list. This sets the membership status to CLOSED so no new users can be added. Existing members will expire based on the membership lifespan.

### `delete({channel:"google", resource:"negative_keyword", params:{…}})`

Remove a negative keyword from a Google Ads campaign. This allows ads to show for that search term again.

### `delete({channel:"google", resource:"negative_keyword_list", params:{…}})`

Delete a shared negative keyword list. This unlinks it from all campaigns. WARNING: Cannot be undone.

### `delete({channel:"tiktok", resource:"ad_group", params:{…}})`

Delete a TikTok Ads ad group. This sets the ad group status to DELETE and stops all ads within it from delivering. WARNING: This cannot be easily undone.


## `duplicate` — write · 3 routes

### `duplicate({channel:"meta", resource:"ads", params:{…}})`

Bulk-duplicate EXISTING ads: copy N source ads into M target ad sets in a single call. WHEN TO USE THIS: you already have working ads (with creatives Meta has approved) and want to fan them out across more ad sets — same creative, more placements / targeting / campaigns. Equivalent to selecting multiple ads in Ads Manager and duplicating them into multiple ad sets at once. Creates source_ad_ids.length × target_adset_ids.length new ads (max 500 per call). Uses Meta`s native /{ad-id}/copies endpoint under the hood — the same path Ads Manager uses. Preserves every ad-level field automatically: partnership / branded_content (partner page tag), tracking_specs, conversion_specs, creative reference, and anything else Meta carries over. REBUILD FALLBACK: when Meta refuses to /copies a source (most commonly because the source creative still carries the deprecated `standard_enhancements` Advantage+ bundle that Meta dropped in MAPI v22 / Jan 2025), the duplicate falls back to a manual rebuild that reconstructs the FULL creative from a detail=full read: Advantage+ enrollment, site links + pinned product set, the multi-media pool (media_sourcing_spec), format display options, text variations, and tracking specs. Such ads appear with `rebuilt: true`; if the rebuilt copy is still missing anything the source had, the entry also carries `fidelity_warnings` naming exactly what was lost — surface those to the user. Partnership / branded-content sources that Meta refuses to copy are rejected with a clear "cannot be duplicated without partnership authorization" error (a rebuilt creative cannot carry the partner's permission). WHEN NOT TO USE: (a) If the ads are net-new (you have image_hash / video_id / IG post id but no existing ad referencing that creative yet), use create({channel:"meta", resource:"ad", mode:"bulk"}) instead — it builds new creatives in bulk and supports the same shapes (5x text rotation, feed/story split, carousels, etc). (b) If you want to recreate the ad with NEW text / link / image (the "remake" workflow), don`t use this — use query({channel:"meta", resource:"ad", mode:"get"}) to read the source (including creative.branded_content), then create({channel:"meta", resource:"ad", mode:"bulk"}) with your overrides and pass branded_content through verbatim. Uses Meta`s Batch API under the hood (50 ops/batch, parallel server-side), so this is dramatically faster than calling create({channel:"meta", resource:"ad", mode:"single"}) in a loop. RESPONSE: { summary: {requested, created, skipped_existing, failed}, created: [...], skipped_existing: [...], errors: [...] }. summary is the source of truth for counts — report it to the user. Partial failures are expected; inspect errors[]. New ads are PAUSED by default. SELF-HEALING / TIMEOUTS: /copies ops that time out inside Meta`s batch are automatically verified against the target ad set and retried; such copies appear in created[] flagged verified_after_timeout or retried — they are REAL, do not re-duplicate them. The call is also IDEMPOTENT: re-running the exact same call skips source×target pairs whose copy already exists in the target ad set (matched by name, the source ad itself excluded) and reports them in skipped_existing — so after a client-side timeout, just call it again with the same arguments. To INTENTIONALLY create a second copy of the same ad in the same ad set, pass a name_template so the new copy gets a distinct name. Use cases: scaling a winning ad across a campaign`s ad sets, copying ads from one campaign into another after building the ad-set shells, duplicating into a new audience test.

### `duplicate({channel:"meta", resource:"adset", params:{…}})`

Duplicate a Meta ad set. Defaults to shallow (ad-set shell only, no ads). Set include_children=true for a deep copy that also clones every non-archived ad under it. Set include_ads=false with include_children=true to clone the shell only (equivalent to default). All new objects are created PAUSED. Optionally pass target_campaign_id to copy into a different campaign. Returns { adset: {from, to}, ads: [...], errors: [...] }.

### `duplicate({channel:"meta", resource:"campaign", params:{…}})`

Duplicate a Meta campaign. Defaults to shallow (campaign shell only). Set include_children=true for a deep copy that also clones every non-archived ad set AND its ads (mirroring what Ads Manager does). Set include_ads=false with include_children=true to clone ad sets only. All new objects are created PAUSED. Returns an id map { campaign: {from, to}, adsets: [...], ads: [...], errors: [...] } so you can verify the copy.


## `media` — write · 20 routes

### `media({channel:"meta", action:"upload_video", params:{…}})`

Upload a SINGLE ad-hoc video to Meta from a public URL. Returns video_id. IMPORTANT: If the user uploaded videos via the Xylo dashboard, OR you have multiple videos to send to Meta, use media({channel:"meta", action:"finalize"}) instead — it bulk-pushes all staged assets in one call. Do NOT loop this tool over a list of videos; that is slow and wastes rate limit. Use this tool only for one-off uploads from an external URL. After uploading, use query({channel:"meta", resource:"ad_video", mode:"get"}) to poll until the video is ready before using it in ads.

### `media({action:"generate", params:{…}})`

Generate NEW ad images with AI (FAL-hosted models: gpt-image-edit, nano-banana-pro, nano-banana-pro-edit, nano-banana, nano-banana-edit, gpt-image, flux-pro-ultra, flux-kontext, recraft-v3). FIRST read knowledge({topic:"creative_generation"}) to pick the right model and prompting technique, and accounts({action:"read_brief"}) for the brand profile + brand asset ids. Static product/promo ads with in-image text → gpt-image-edit (GPT Image 2: best-in-class text/layout, all key ratios incl. 4:5/9:16). Both GPT Image 2 models take quality (default "medium", 20cr/image) — if small text or logos come out imperfect, retry the SAME call with quality:"high" (45cr/image). Pass brand_asset_ids (logo, exact product shots) as reference images on edit-capable models so output stays on-brand. num_images gives near-identical variants of ONE prompt — for distinct concepts make separate calls with different prompts. If the request needs an exact product/logo that is NOT in brand_assets yet, acquire it first (winning ad media, brand website, or user upload) — the knowledge topic has the checklist; never generate an approximation from a text description. Costs generation credits (per-model price; response reports remaining). Outputs are staged like uploads — preview the URLs, then media({action:"finalize"}) to push to Meta and use in ads.

### `media({action:"generate_video", params:{…}})`

Generate a NEW ad video with AI (kling-video = dialogue/lip-sync/4K workhorse, seedance-video = premium cinematic + richest references, omni-video = cheap drafts + the only EDITABLE output). FIRST read knowledge({topic:"video_generation"}) — model routing, per-model prompting, credit costs (video is 10-70x image cost; confirm with the user before calls ≥500 credits), and the stills-first precision workflow. ASYNC: returns a job_id immediately; generation takes 2-5 min — tell the user, then poll media({action:"video_status"}). For precise control: generate the exact frame as an IMAGE first (media generate), then animate it via first_frame_asset_id. Costs credits per second (charged on submit, auto-refunded on failure).

### `media({action:"edit_video", params:{…}})`

Edit an existing video with natural language (Gemini Omni — the ONLY video editor): swap/remove objects, change style/lighting, on an uploaded video or a prior generation. When a generated video is "close but change one thing", edit it — do NOT re-roll. Up to 3 chained edit turns; ONE change per call, end the prompt with "Keep everything else the same." Source must be ≤10s and must not show recognizable real people. Costs 25 credits/second of the source; async — poll media({action:"video_status"}).

### `media({action:"video_status", params:{…}})`

Check a video generation/edit job. Poll every 30-60s after generate_video / edit_video (jobs take 2-5 min). Returns the staged asset (preview URL) when done; failures are auto-refunded. If a generate call ever times out, the job was still created and charged — find it here or in query({resource:"uploaded_media"}) BEFORE re-rolling.

### `media({action:"save_brand_asset", params:{…}})`

Save a DURABLE reference asset (logo, exact product shot, style reference, or screenshot) to this account's brand — shared across every account attached to that brand, and unlike staged uploads brand assets never expire. Used as reference images for media({action:"generate"}) (pass the returned id in brand_asset_ids) and surfaced in accounts({action:"read_brief"}) as part of the brand's saved context. Provide a public image URL; the asset is downloaded, deduped by content hash, and stored. Give it a short descriptive label (e.g. "Primary logo", "Hero product — blue can").

### `media({action:"save_winning_ad", params:{…}})`

Import a winning ad into the brand library as a PLATFORM REFERENCE (no media duplication). Dedupes by creative: two ads running the same image/video merge into one entry (ad_ids union). Server-side it finds every ad using the creative, pulls 30-day stats, runs creative analysis, and writes a dictionary description. Re-running refreshes stats/analysis (idempotent). Use during account research (top <=20 winners) and on later library-refresh passes. MULTI-MEDIA ADS (an ad carrying several uploaded videos/images): do NOT save the whole ad — its winner may be ONE asset. First break the ad down by asset with insights({lens:"ad", breakdowns:"video_asset"} or "image_asset") to see per-asset spend/results, then call this ONCE PER WINNING ASSET with media_index (0-based, matches the ad's media roster) + that asset's own per-asset metrics_snapshot from the breakdown. With media_index set the server saves and analyzes JUST that asset and trusts your metrics_snapshot (it will NOT overwrite it with the whole-ad rollup, which would over-credit a weak asset).

### `media({action:"list_winning_ads", params:{…}})`

List the brand's winning-ads library (lean: refs, name, description, 30d stats). detail:"full" adds the stored creative analysis. Use before a refresh pass to see what is already saved.

### `media({action:"update_winning_ad", params:{…}})`

Edit a saved winning ad's name, headline, description, or metrics snapshot WITHOUT re-running analysis. To refresh analysis/stats, call save_winning_ad again (it upserts).

### `media({action:"delete_winning_ad", params:{…}})`

Remove a winning ad from the brand library (the platform ad itself is untouched).

### `media({action:"get_asset", params:{…}})`

Resolve a copy-pastable asset ref the user hands you: brand_asset:{id}, winning_ad:{id}, or generated:{id} (shown in the dashboard's "Copy ID for Claude"). Refs are globally unique, so the ref alone is enough; platform and account_id are optional and unnecessary. Returns the full record: description, refs, display URL, analysis (winning ads) or generation prompt (generated images).

### `media({channel:"meta", action:"stage_from_catalog", params:{…}})`

Pull a Meta catalog product's photos (main image + additional images — often multiple studio angles) into 24h Xylo staging so they can be used as reference images for AI creative generation. Free — no generation credits are charged for staging. WORKFLOW for e-com/physical-product creative: (1) Find the product: query({channel:"meta", resource:"catalog", mode:"list"}) then query({channel:"meta", resource:"catalog_product", mode:"list", params:{q:"<product name>"}}). (2) Call this with the product id. The result includes an inline preview of EVERY staged image. (3) LOOK at each preview and vet it: keep only clean product shots — product on a plain/studio background, no text overlay, no heavy lifestyle clutter — preferring shots of the product from DIFFERENT angles (up to ~4 is ideal). Ignore duds; they expire on their own in 24h. (4) Pass the kept asset ids as media_asset_ids to media({action:"generate"}), and write the prompt from the product's real catalog name/description/brand for accuracy. Assets expire after 24h — re-stage on demand for the next generation session.

### `media({channel:"meta", action:"upload_image", params:{…}})`

Upload a SINGLE ad-hoc image to Meta and get back an image_hash for ad creation. IMPORTANT: If the user uploaded images via the Xylo dashboard, OR you have multiple images to send to Meta, use media({channel:"meta", action:"finalize"}) instead — it bulk-pushes all staged assets in one call. Do NOT loop this tool over a list of images; that is slow and wastes rate limit. Use this tool only for one-off uploads from an external URL or base64 blob (e.g., a single AI-generated image). Provide either an image_url (publicly accessible URL) or image_data (base64 encoded). Supported formats: JPG, PNG. Recommended size: 1200x628 pixels for feed ads.

### `media({channel:"meta", action:"bulk_upload_urls", params:{…}})`

Create upload URLs so the client can stream images/videos directly into the Xylo dashboard (staging only — this does NOT upload to Meta). Handles ONE file or MANY: for a single file, pass a one-element `files` array; for many, it collapses what would be dozens of round trips into 1. Works in Claude Code, Claude Desktop, and claude.ai web (any environment that can make an HTTP PUT). WORKFLOW: (1) Call this tool with ad_account_id + an array of {filename, mime_type, group?}. (2) PUT each file's bytes to its returned upload_url (Bash + curl, or any HTTP client). The Content-Type header MUST match the registered mime_type. (3) Each PUT response is the registered media asset (asset_id, blob_url, dimensions, group_id, etc.), staged in the Xylo dashboard (not yet on Meta). (4) Optional: call query({channel:"meta", resource:"uploaded_media", mode:"list"}) to confirm groupings, then media({channel:"meta", action:"finalize"}) to push to Meta. PRE-GROUPING: pass an optional `group` label on each file to stage assets already grouped — files that share a label (2+ of them) land in the same creative set with that label as the group name, with no separate merge step. Use this to pair placement variants you have decided belong together (e.g. a 1:1 feed image and its 9:16 story version). Distinct labels = distinct groups; a label on a single file is ignored. AUTO-PAIRING: files staged WITHOUT a group label auto-pair on upload when a feed-size and a 9:16 story-size asset share a filename (placement markers like feed/story/reel/1x1/4x5/9x16/1080x1920 are ignored when comparing) or look visually identical. Explicit labels always win over auto-pairing. Caps: 100 files per call, 30MB per image, 300MB per video. Upload URLs expire in 1 hour.

### `media({channel:"meta", action:"upload_directory", params:{…}})`

For agents with local filesystem access (Claude Code, Cursor, etc.). Upload every image/video in a local directory into the Xylo dashboard as a staged, PRE-GROUPED creative set — ready for the user to review. This stages into Xylo only; it does NOT push to Meta. Skip this tool when running in claude.ai (no filesystem) — use the Xylo dashboard upload instead. This is the efficient one-pass flow: review the creatives, decide the groupings yourself, and stage them already grouped + named so the user can open the dashboard and double-check accuracy — no separate upload-then-group-then-rename round trips. WORKFLOW (the agent executes these steps): (1) List the image/video files in `directory` (jpg, jpeg, png, mp4, mov). (2) REVIEW the images (read/open them) and decide which files are placement variants of the same     creative — e.g. a 1:1 or 4:5 feed image paired with its 9:16 story/reel version. Give each set a     short descriptive name based on the creative content (not the placement sizes). (3) Call this tool with that plan: `groups` (each {group_name, files}) plus any leftover `ungrouped`     files. This tool calls Xylo and returns the per-file upload URLs (annotated with their group). (4) PUT each file to its returned upload_url in parallel (Bash + curl or any HTTP client).     Content-Type must match mime_type. Each PUT response is the registered, already-grouped asset. (5) Tell the user the creatives are staged and grouped in the Xylo dashboard under this ad account,     and to review the groupings there. (6) Only after the user confirms, push them to Meta with media({channel:"meta", action:"finalize"}). Caps: 100 files per call, 30MB per image, 300MB per video. Upload URLs expire in 1 hour.

### `media({channel:"meta", action:"group", params:{…}})`

Group already-staged media assets into a creative set inside the Xylo dashboard. Use this after viewing uploaded images with query({channel:"meta", resource:"uploaded_media", mode:"list"}) to pair placement variants together (e.g., a 1:1 feed image with its 9:16 story version) when they were not pre-grouped at upload time. Pass the asset IDs of images that are the same creative in different sizes. Requires at least 2 asset IDs. All assets must belong to the same ad account. This only regroups assets staged in Xylo — it does not touch Meta. After grouping, tell the user they can review the groupings in the Xylo dashboard under their ad account tab. Then ask if they would like you to send the assets to Meta now so they can be used in ads (via media({channel:"meta", action:"finalize"})), or if they want to review and adjust first.

### `media({channel:"meta", action:"rename_group", params:{…}})`

Rename a creative set / media group staged in the Xylo dashboard. Use this after grouping assets with media({channel:"meta", action:"group"}) to give the group a descriptive name based on the creative content (e.g., "Summer Sale - Pool Service", "Brand Awareness - Clock Ad"). The group_id is returned by media({channel:"meta", action:"group"}) or visible in query({channel:"meta", resource:"uploaded_media", mode:"list"}) results.

### `media({channel:"meta", action:"finalize", params:{…}})`

Push staged creative assets (images and videos) from the Xylo dashboard to Meta in a single call. THIS is the step that actually uploads to the live Meta ad account — everything before it (media({channel:"meta", action:"bulk_upload_urls"}), media({channel:"meta", action:"upload_directory"})) only stages into Xylo. It is the batch equivalent of pressing "Send to Meta" in the dashboard — use it whenever the user has staged multiple images or videos and you need to push them to the Meta ad account so they can be used in ads. ALWAYS prefer this over calling media({channel:"meta", action:"upload_image"}) / media({channel:"meta", action:"upload_video"}) in a loop: it sends 10, 20, 50+ assets at once instead of one-by-one (much faster, fewer rate-limit risks). Call this after the user confirms their groupings are correct. You can finalize all unfinalized assets (default) or specific ones by ID. Assets already finalized (meta_ref is not null) are skipped automatically. TIMEOUTS: uploads run in parallel server-side, but a very large batch can still exceed the client timeout — the server KEEPS uploading after the connection drops. If this call times out, re-run the SAME call (already-finalized assets are skipped) and check state with query({channel:"meta", resource:"uploaded_media", mode:"list"}) until every asset has a meta_ref. Returns each asset's meta_ref (image_hash for images, video_id for videos) — pass these directly into create({channel:"meta", resource:"ad", mode:"single"}).

### `media({channel:"tiktok", action:"upload_video", params:{…}})`

Upload a video to the TikTok Ad account library. Either pass video_url (TikTok will pull) or file_b64 + file_name for inline upload. Returns video_id used in ad creative.

### `media({channel:"tiktok", action:"upload_image", params:{…}})`

Upload an image creative to the TikTok Ad account library. Pass image_url or file_b64 + file_name.


## `publish` — write · 10 routes

### `publish({channel:"meta", resource:"instagram_media", action:"publish", params:{…}})`

Publish content to an Instagram account. Set `media_type`: "IMAGE" (a single JPEG photo via image_url), "VIDEO" (a feed video via video_url), "REEL" (a short-form Reel via video_url — may be surfaced to non-followers), "STORY" (a 24-hour story — provide image_url OR video_url, not both), or "CAROUSEL" (2–10 items via `items`, each with an image_url or video_url). Media must be publicly accessible via URL; only JPEG images are supported. Videos, reels, and carousels containing video may take up to ~60s to process before going live. Instagram enforces a rate limit of 100 posts per 24 hours across all content types (check it with query({channel:"meta", resource:"instagram_publishing_limit", mode:"get"})). Keywords: post to Instagram, publish photo, publish image, publish video, publish reel, publish story, publish carousel.

### `publish({channel:"meta", resource:"comment", action:"settings", params:{…}})`

Enable or disable comments on an Instagram media post. When comments are disabled, existing comments are hidden and new comments cannot be added. Requires the IG Business account ID that owns the post so the Page access token can be resolved.

### `publish({channel:"meta", resource:"page_post", action:"create", params:{…}})`

Create a post on a Facebook Page — published immediately, or scheduled for a future time. You can post plain text, include a link preview, or attach an image URL. By default the post publishes right away; to SCHEDULE it for later instead, pass scheduled_publish_time (the post stays queued until then — review queued posts with query({channel:"meta", resource:"page_post", mode:"list"})). To use a creative uploaded via the Xylo dashboard, call query({channel:"meta", resource:"uploaded_media", mode:"list"}) and pass the asset's `preview_url` as `image_url` — do NOT pass the image_hash (meta_ref), which is only valid for ads. Keywords: post to Facebook, publish Page post, schedule Facebook post, queue a post for later.

### `publish({channel:"meta", resource:"page_post", action:"update", params:{…}})`

Update the text message of an existing Facebook Page post (published or scheduled). Only the message field can be changed after creation — link and image cannot be swapped. Use the post_id returned from create_page_post or list_scheduled_posts.

### `publish({channel:"meta", resource:"page_post", action:"delete", params:{…}})`

Delete a Facebook Page post (published or scheduled). WARNING: This permanently removes the post and cannot be undone. Published posts will be removed from the Page timeline immediately. Scheduled posts will be cancelled and will not publish.

### `publish({channel:"tiktok", resource:"comment", action:"settings", params:{…}})`

Configure TikTok ad comment moderation: enable/disable comments globally, set auto-hide keywords, block specific user IDs.

### `publish({channel:"tiktok", resource:"tcm", action:"invite", params:{…}})`

Invite TikTok creators to a TTCM (Creator Marketplace) campaign for branded content. Pass campaign_id, creator_ids, optional budget/message.

### `publish({channel:"meta", resource:"comment", action:"moderate", params:{…}})`

Moderate and respond to comments on Meta surfaces: Instagram media (posts/reels) and Facebook Page posts. Set `surface` ("instagram" or "page") and `action`: "create" (post a new top-level comment on an Instagram media_id as the IG Business account — Instagram only), "reply" (reply to an existing comment_id — a threaded child reply on Instagram, or a reply as the Page on a Page post), "hide" (Instagram only: hide or unhide a comment_id via the `hide` flag — hidden comments stay but are not shown to others), or "delete" (permanently remove a comment_id — cannot be undone; prefer "hide" on Instagram if unsure; on Pages use only for spam, hate speech, or policy violations). page_id is always required for Page actions; ad_account_id is required on every call. To READ comments, use the comment list route (query, mode "list") instead. Keywords: reply to Instagram comment, reply to comment as the Page, respond to comments, hide comment, delete/moderate IG or Facebook Page comments, comment moderation, audience engagement.

### `publish({channel:"tiktok", resource:"comment", action:"moderate", params:{…}})`

Moderate comments on the advertiser's own TikTok video ads. comment_id is always required; set `action`: "reply" (reply to a first-level comment — requires `text` plus the originating ad_id + tiktok_item_id and the identity that owns the ad), "hide" (hide the comment — the commenter still sees it, others don't), "delete" (permanently remove the comment — requires the originating ad_id + tiktok_item_id and the identity that owns the ad), or "like" (like the comment — boosts perceived engagement). Find comment ids first with query({channel:"tiktok", resource:"comment", mode:"list"}). Keywords: reply to TikTok ad comment, hide TikTok comment, delete TikTok comment, like comment, moderate TikTok ad comments, comment moderation, audience engagement.

### `publish({channel:"tiktok", resource:"tcm", action:"review", params:{…}})`

Review a TTCM (TikTok Creator Marketplace) creator's draft video for a branded content order: set `decision` to "approve" (feedback optional) or "reject" (feedback REQUIRED — tell the creator what to change). Requires the TTCM order_id (find it via query({channel:"tiktok", resource:"tcm_order", mode:"list"})). Keywords: approve creator draft, reject draft video, TTCM branded content review, influencer collaboration, creator marketplace draft feedback.


## `tracking` — write · 3 routes

### `tracking({channel:"meta", action:"send_events", params:{…}})`

Send conversion events to Meta via the Conversions API (CAPI). Use this to send server-side events that complement or replace browser pixel events. Supports standard events (Purchase, Lead, AddToCart, etc.) and custom events. IMPORTANT: Send user data (email, phone, name) as plain text — the server automatically SHA-256 hashes PII before sending to Meta. Do NOT pre-hash values. Fields like client_ip_address and fbc are sent unhashed. Maximum 1,000 events per request.

### `tracking({channel:"google", action:"upload_offline", params:{…}})`

Upload offline click conversions to Google Ads. Each conversion requires a gclid (Google Click ID), conversion action ID, date/time, value, and currency. Use this to attribute offline sales, phone orders, or CRM events back to Google Ads clicks.

### `tracking({channel:"tiktok", action:"send_events", params:{…}})`

Send conversion events to TikTok via the Events API 2.0 (server-side tracking, the TikTok equivalent of Meta Conversions API / CAPI). Pass `event_source`: web | app | offline | crm and an `events` array. PII (email/phone) is automatically SHA-256 hashed.


## `automation` — write · 4 routes

### `automation({channel:"meta", resource:"budget_schedule", action:"create", params:{…}})`

Schedule a budget change for a fixed window on a Meta campaign or ad set. Use ABSOLUTE to set a new daily/lifetime budget for the window (value in minor currency units, e.g. cents). Use MULTIPLIER to scale the existing budget (value is percent: 150 = 1.5x, 300 = 3.0x, range 100–1000). Returns the new schedule id. Example: promote Black Friday by setting MULTIPLIER=200 from 2026-11-27 to 2026-11-30.

### `automation({channel:"meta", resource:"budget_schedule", action:"delete", params:{…}})`

Delete a budget schedule by ID.

### `automation({channel:"meta", resource:"ad_rule", params:{…}})`

Create and manage Meta AUTOMATED RULES (the Ad Rules Engine) — rules that watch ad objects on a schedule and act automatically. COMMON USE CASES: auto-pause underperforming ads/ad sets when CPA is too high or ROAS too low, auto-scale (increase) budget when ROAS is good, dayparting / scheduling (only run certain hours/days), get an alert or notification when spend/CPA crosses a threshold, rotate ads, rebalance budget across ad sets toward the best performers, or ping a webhook on a trigger. Set `action`: "create", "update", "delete", or "enable"/"disable" (turn a rule on/off — maps to status ENABLED/DISABLED). SAFETY: a newly created rule defaults to DISABLED so it never acts on live spend before you review it — turn it on afterward with action "enable" (or status ENABLED). ENABLE/DISABLE ARE SELF-VERIFYING: Meta applies a status change with a lag and can silently drop the first write, so enable/disable (and any status update) re-read the rule and re-issue the write until the new status is confirmed, then return {success, verified, status}. A rare verified:false + warning means Meta is lagging (not a rejection) — just call the same action again. TWO ways to define a rule on create/update: (A) ERGONOMIC (schedule rules, the common case): `name`; `applies_to` {level: AD|ADSET|CAMPAIGN, ids?} (REQUIRED when not passing a raw evaluation_spec — Meta needs entity_type or id on every rule); `conditions` [{metric, operator, value}] + `time_window` (a time_preset like LAST_7D — REQUIRED whenever conditions are present); `rule_action` {type, change?, rebalance?, ...}; and `schedule` (DAILY | HOURLY | SEMI_HOURLY | a custom array). (B) RAW escape hatch for FULL parity (incl. trigger rules + advanced/aggregate/formula filters): pass `evaluation_spec`, `execution_spec`, and/or `schedule_spec` as Meta-shaped objects — a raw spec is used VERBATIM and OVERRIDES the ergonomic params. UNITS: money in `conditions` thresholds and in `rule_action.change.amount` (when unit=ACCOUNT_CURRENCY) is in the currency BASE UNIT — CENTS for USD (2000 = $20). Values pass through unchanged to match Meta. NOTE: rules that PAUSE objects cannot filter on cost metrics (Meta error 2703). No manual run / dry-run exists in the Meta API. SCHEDULED ON/OFF REALITY: (1) an UNPAUSE rule ALWAYS shows a "paused/eligible objects" condition in the Ads Manager UI — Meta auto-injects a status NOT_IN [DELETED, ARCHIVED] filter on every UNPAUSE rule; that is expected, not a misconfiguration. (2) The rules engine has NO calendar-DATE and NO one-time schedule — only recurring times (DAILY / HOURLY / SEMI_HOURLY, or a CUSTOM array of minute-of-day + day-of-week). For a true one-time launch at a specific date/time, prefer setting a future start_time on a NOT-yet-started campaign/ad set; use a scheduled UNPAUSE rule when you want a repeating on/off pattern. Rules scoped to explicit ids use the level id field (campaign.id / adset.id / ad.id) so they render as that object, not "All". update REPLACES the whole spec you send — re-supply applies_to whenever you change conditions. ad_account_id is REQUIRED on every call (e.g. act_1234567890).

### `automation({channel:"meta", resource:"value_rule_set", params:{…}})`

Create and manage Meta VALUE RULES (value rule sets) — bid multipliers that pay more (or less) for specific audiences, attached at the ad-set level. COMMON USE CASES: bid a multiplier by age, gender, location, placement, device, or OS; pay more for high-value audiences / segments worth more to you; lifetime-value (LTV) bidding; raise bids for your best converting locations or placements and lower them elsewhere. Set `action`: "create", "update" (full read-modify-write — pass the FULL desired rule list, keeping ids for rules you retain), "delete", "attach" (apply a value rule set to an ad set), or "detach" (remove value rules from an ad set). On create/update pass `name` + `rules` — each rule is {name, adjust_sign (INCREASE/DECREASE), adjust_value (percent: INCREASE 1–1000, DECREASE 1–90), criterias:[{criteria_type, operator:"CONTAINS", criteria_values, criteria_value_types}]}. The wire key is "criterias" (Meta's spelling). LIMITS: 6 rule sets/account, 10 rules/set, 4 criteria/rule; first-matching-rule wins (array order = priority). attach needs adset_id + value_rule_set_id; detach needs adset_id only. ELIGIBILITY: ad sets on auto-bid (LOWEST_COST_WITHOUT_CAP) or COST_CAP. ad_account_id is REQUIRED on every call (e.g. act_1234567890).


## `connect` — write · 3 routes

### `connect({channel:"meta", action:"connect", params:{…}})`

Start the flow to connect a new Meta ad account. Returns a connect_url that the user must visit to authorize access via Meta OAuth. The URL expires in 30 minutes. Use this when the user wants to add a new ad account to Xylo.

### `connect({channel:"meta", action:"disconnect", params:{…}})`

Disconnect (remove) a Meta ad account from the Xylo organization. This revokes Xylo's access to the account. The account can be reconnected later. WARNING: This action cannot be undone without re-authorizing.

### `connect({channel:"tiktok", action:"authorize_identity", params:{…}})`

Authorize a TikTok user (TT_USER identity) for Spark Ads via an auth_code obtained from the in-app TikTok auth flow. Use this so the advertiser can promote a creator's organic post as a Spark Ad.


## Named AI tools & utilities

### `audit_campaign`

Audit one or more ad campaigns against industry benchmarks. Analyzes CTR, CPC (all), CPM, ROAS, conversions, and spend patterns. Returns a score (0-100) and structured findings with severity levels (critical, warning, info, good) and actionable recommendations. The audit accounts for the "Breakdown Effect" — Meta's Advantage+ system optimizes for marginal efficiency (cost of the next conversion), not average efficiency. Higher CPM/CPC with strong ROAS is expected behavior, not a problem. NOTE: benchmark scoring counts STANDARD events (purchases, leads). For ad sets whose promoted_object points at a custom event or Custom Conversion, cross-check conclusions against that optimized event (query({channel:"meta", resource:"adset", mode:"get"}) + insights with include_actions=true) before acting on them. Recommendations are framed as testable hypotheses, not directives. This audit accounts for learning phase status, bid strategy context, and the breakdown effect. For a more comprehensive structural audit, use morpheus_audit instead. For deep understanding of the mechanics behind findings, call knowledge({topic:"meta_ads"}).

### `optimize_budget`

Get budget optimization recommendations for your campaigns. Analyzes ROAS, CPA, and CTR efficiency across campaigns and suggests how to redistribute budget. IMPORTANT: If campaigns use Advantage+ Campaign Budget (CBO), Meta already optimizes budget allocation using marginal efficiency. These recommendations are based on average metrics and may differ from Meta's ML optimization. OPTIMIZED EVENT: the ROAS/CPA inputs count STANDARD events. Before applying recommendations, read the affected ad sets' promoted_object (query({channel:"meta", resource:"adset", mode:"list"})) — campaigns optimizing for a custom event or Custom Conversion must be judged on that event (insights with include_actions=true), or these recommendations can shift budget away from what actually converts. Test budget changes incrementally (10-20% at a time) rather than making large shifts. Bid strategy matters: cost cap campaigns throttle delivery when CPA exceeds target; highest volume campaigns maximize spend regardless. For pacing and bid strategy context, call knowledge({topic:"meta_ads"}).

### `generate_report`

Generate a comprehensive campaign performance report. Includes executive summary, key metrics, performance assessment against benchmarks, top and bottom performers, and recommendations framed as testable hypotheses. Defaults to the last 30 days. If the date range includes today, the report notes that today's data is partial. Great for weekly or monthly performance reviews.

### `morpheus_audit`

Run a comprehensive Morpheus Media RMP (Rating & Methodology Platform) audit on a Meta or Google Ads account. This goes far beyond basic benchmarking — it checks campaign structure, retargeting configuration, creative health, keyword organization, bidding strategy, conversion tracking, audience strategy, and more against Morpheus Media's best practices from 13+ client audits. Returns an overall score (0-100), category scores, and specific findings with severity levels and actionable recommendations. For Meta: checks prospecting consolidation, retargeting ADV+ settings, audience overlap, creative fatigue (>90 days), CTR decline trends, bidding optimization (max value vs max conversions), attribution settings, pixel health, and more. For Google: checks branded campaign separation, budget allocation, keyword count per ad group (<7 best practice), branded queries in non-branded campaigns, PMax segmentation (80/20 rule), broad match in branded campaigns, conversion action duplicates, RSA completeness, and remarketing presence. IMPORTANT: For Meta audits, pass the ad_account_id. For Google audits, pass the google_customer_id. Meta audits now include learning phase detection, auction overlap analysis, bid strategy interpretation, ad relevance diagnostics, and performance fluctuation analysis. Findings are contextualized — ad sets in learning phase won't generate false performance warnings. For the full Meta ads knowledge framework, call knowledge({topic:"meta_ads"}).

### `send_feedback`

Send feedback, bug reports, or issue reports about Xylo to the Xylo team. CALL THIS TOOL WHENEVER: a Xylo MCP tool returns an unexpected error, returns wrong/incomplete data, or you find yourself stuck or repeatedly retrying because Xylo behavior is unclear or broken; an action the user asked for cannot be completed because of a Xylo-side problem; the user explicitly says "report this", "send feedback", "this is broken", "tell the Xylo team", "file a bug", "this isn't working", or asks you to surface a complaint, suggestion, or pain point. Also call proactively at the end of a frustrating session to summarize what went wrong, so the Xylo team can fix it. Reports are emailed to the support team with the full context you provide. Be detailed: include the exact error messages from previous tool responses, the tools you tried, what the user was trying to accomplish, and any relevant IDs (campaign IDs, ad account IDs, etc.). You do NOT need user confirmation to send feedback when issues happen — just send it.

### `describe`

Get the exact params schema for any route: no args → capability overview; `{tool}` → that dispatcher's route list; `{tool, ...discriminators}` → one route's JSON Schema + example; batch via `routes:[...]`.

### `search_tools`

Search every ad operation by intent; returns ranked routes with the discriminators to pass.
