← Realize PluginCONTENT HISTORY
WHAT CHANGED
Update to Realize Plugin
Snapshot Nov 10, 2026 · 00:10 UTC · version 0.5.5
Collection source: downloaded plugin package.
First saved snapshot
No earlier snapshot is available to establish a change.
Save Realize Plugin and check future changes in Following.
Up to 3 plugins free · no card required.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Create and update Realize campaigns, native + display items, and account-level conversion rules via the Realize MCP write tools. Activates on any write-intent request — create, edit, pause/resume, launch, budget changes, bid changes, targeting edits, creative swaps, conversion-rule creation, attribution-window changes, retiring a conversion rule. Enforces a tiered preview-then-confirm pattern with a mandatory account-identity header on every confirmation so the user always sees which account is being mutated. Falls back to a UI reference for actions the MCP does not currently expose (delete, duplicate, bulk operations, pixel installation, codeless-conversion setup, pixel test-fire). Grounded in the realize-toolkit's setup guidance and Taboola's official setup guide for the input-validation heuristics (Marketing Objective enum, Bid Strategy × budget minimums, learning-phase defaults).",
"included_files": [
{
"relative_path": "references/mcp-write-surface.md",
"size_in_bytes": 27769
}
],
"name": "manage-campaigns",
"skill_md_contents": "---\nname: manage-campaigns\ndescription: Create and update Realize campaigns, native + display items, and account-level conversion rules via the Realize MCP write tools. Activates on any write-intent request — create, edit, pause/resume, launch, budget changes, bid changes, targeting edits, creative swaps, conversion-rule creation, attribution-window changes, retiring a conversion rule. Enforces a tiered preview-then-confirm pattern with a mandatory account-identity header on every confirmation so the user always sees which account is being mutated. Falls back to a UI reference for actions the MCP does not currently expose (delete, duplicate, bulk operations, pixel installation, codeless-conversion setup, pixel test-fire). Grounded in the realize-toolkit's setup guidance and Taboola's official setup guide for the input-validation heuristics (Marketing Objective enum, Bid Strategy × budget minimums, learning-phase defaults).\nallowed-tools: [\"Read\", \"Grep\", \"AskUserQuestion\"]\n---\n\n# Manage Campaigns\n\nWrites for Realize campaigns and native + display items via MCP. Every destructive call is gated behind a preview and an explicit user confirmation; the preview always identifies the target account by name and opaque `account_id`. This skill is the only place in the plugin that calls the MCP write tools.\n\n**Depth:** the full field-by-field MCP write-surface reference (every scalar on `create_campaign`, every field on `create_native_item` / `create_display_item`, per-strategy bid-lever gates, common payload patterns, failure modes) lives in `references/mcp-write-surface.md`. Read it when a payload needs detailed field coverage or when debugging a write rejection.\n\n## When to use\n\nTrigger on any of: *create, make, set up, launch, edit, update, change, pause, resume, duplicate, clone, delete, remove, bump the budget, raise the bid, lower CPC, swap the creative, add an ad, add a creative, target X, exclude Y*.\n\nIf the user is *asking* about a campaign rather than changing it, route to the `campaigns` skill (reads only). If the user is *diagnosing* performance, route to `optimize-campaign` — that skill hands write prescriptions back here.\n\n## Prerequisites\n\n- `account_id` resolved via the `accounts` skill. If missing, hand off there first.\n- For **updates**, the relevant `get_campaign` / `get_item` read MUST precede the write — both to render a diff preview and (for `update_campaign` targeting blocks) to merge client-side so the full-replace semantics do not wipe dimensions the user didn't mention.\n\n## Tools this skill wraps\n\n| Tool | Required params | Idempotent? | Notes |\n|---|---|---|---|\n| `mcp__realize-mcp__create_campaign` | `account_id`, `name`, `marketing_objective`, `branding_text`, `spending_limit_model`, `bid_strategy` | No | Atomic. Ships PAUSED unless `is_active=true`. 15 optional targeting blocks (full-replace within block). |\n| `mcp__realize-mcp__update_campaign` | `account_id`, `campaign_id` | Yes | **Scalars partial-merge**; **targeting blocks full-replace within a section**. At least one updatable field required. |\n| `mcp__realize-mcp__create_native_item` | `account_id`, `campaign_id`, `url`, `title`, `description`, `thumbnail_url` | No | Optional `branding_text`, `cta`. New items typically PENDING_APPROVAL → RUNNING. |\n| `mcp__realize-mcp__update_native_item` | `account_id`, `campaign_id`, `item_id` | Yes | At least one updatable field required. |\n| `mcp__realize-mcp__create_display_item` | `account_id`, `campaign_id`, `url`, `creative_name`, plus either `ad_tag` (3P JS) or `asset_url` + `dimensions` (1P-hosted) | No | Locks campaign as Display on first call when `pricing_model=CPC`. 3P tags must match the validator allowlist (no `<!DOCTYPE>` / `<html>` wrapper). |\n| `mcp__realize-mcp__update_display_item` | `account_id`, `campaign_id`, `item_id` | Yes | At least one updatable field required. Arrays like `verification_pixel` are full-replace. |\n| `mcp__realize-mcp__create_conversion_rule` | `account_id`, `display_name`, `event_name`, `type`, `category`, `condition`, `look_back_window`, `include_in_total_conversions`, `status`, `effects` | No | **Account-level.** No tool-side defaults — every required field must be supplied. `type` / `category` / `event_name` immutable afterwards. Returns the server-assigned `id`. |\n| `mcp__realize-mcp__update_conversion_rule` | `account_id`, `rule_id` | Yes | **Partial-merge on every field, including `condition` and `effects`** — omitted fields keep their stored value. The only way to retire a rule (`status=DISABLED` / `ARCHIVED`); there is no delete tool. |\n\nFor previews and merges, this skill also reads `mcp__realize-mcp__get_campaign`, `mcp__realize-mcp__get_item`, and `mcp__realize-mcp__get_conversion_rules`.\n\n## Conversion rules — account-level writes\n\nThese two tools differ from every other write in this skill in ways that matter, so they get their own rules.\n\n**The blast radius is the account, not one campaign.** A conversion rule feeds attribution and, when `include_in_total_conversions` is true, the account's Total Conversions — which is what Target CPA and Maximize Conversions bid against. Editing or retiring a rule can move reported performance and live bidding across every campaign that references it. The preview must name that consequence, not just the field diff.\n\n**Merge semantics are inverted from campaign targeting.** Campaign targeting blocks are *full-replace within a section*, so you must read-and-merge. Conversion rules **partial-merge on everything, including `condition` and `effects`** — so you send only the fields you're changing. Sending a \"complete\" object here is the mistake, and it's the opposite of the reflex the rest of this skill trains.\n\n**Never echo a read payload back as an update.** `get_conversion_rules` returns explicit nulls (`view_through_look_back_window`, `event_name`, condition members) that fail input validation, plus fields the update tool has no parameter for (`id`, `advertiser_id`, `pixel_id`, `exclude_from_campaigns`, `external_id`, `partner`, `tracked_elements`) which are rejected as unknown parameters. Read to see current values; then hand-build a minimal payload of just the edits.\n\n**One ACTIVE rule per event — and stealing the event is forbidden.** Call `get_conversion_rules` before any create, following the paging rules in step 2 of the create flow (`search_text` + exact compare for the name, `status=\"ACTIVE\"` + page to `total` for the event). **A single unqualified call returns 25 rules and is not a check.** If a page still overflows the tool-result cap it comes back as an error plus a **dumped result file** — lower `page_size` and retry first; if you do work from the file, search it for the target `event_name` and `display_name` (with the `Grep` tool). Never skip the check, and never treat an overflow as \"no rules\". If an ACTIVE rule already holds the target `event_name`, the create is rejected. The backend *will* accept a new rule once the incumbent is DISABLED — do **not** take that path on your own initiative. Disabling a live rule to free up its event silently breaks attribution and bidding on every campaign using it. Present the two real options and let the user choose:\n\n> An active rule (**Purchase — main**, ID 3312) already tracks `make_purchase` on this account. I can either update that rule, or retire it and create a new one — retiring it stops conversion reporting for every campaign currently using it. Which do you want?\n\n**`type` / `category` / `event_name` are immutable after creation.** Get them right at create time. Resending one unchanged on an update is accepted; changing one returns a READONLY error — and that error **may name `eventName` even when `type` was the field you actually changed**, so don't chase the field it names.\n\n**Two windows, two different units.** `look_back_window` is click-through in **days** (1–30). `view_through_look_back_window` is view-through in **minutes** (1–10080, where 10080 = 7 days). A user asking for \"a 7-day view-through window\" means `10080`, not `7`. Always state the unit back in the preview in the user's terms *and* the stored value.\n\n**`effects` values are numeric strings.** `[{type: \"REVENUE\", data: \"49.99\"}]` — not the number `49.99`. Required on create; pass `[]` for a rule with no revenue value.\n\n**Two value-reporting fields nobody asks about, and both have defaults that decide something.**\n\n- **`include_in_total_value`** — whether this rule's value counts toward **Total Conversion Value**. When omitted it **defaults to whatever `include_in_total_conversions` is**. So a revenue rule created with `include_in_total_conversions: true` silently opts into Total Conversion Value as well. That is usually what the user wants, but it is a decision made by omission, so state both in the preview and set the field explicitly whenever the user's intent for the two differs.\n- **`aggregation_type`** — `AGGREGATED` sums the values a rule collects; `LAST_VALUE` keeps only the most recent. The backend **defaults to `AGGREGATED`**. On a purchase rule that's right; on something like a quote-estimate or a cart-total that gets re-fired as the user edits it, summing inflates reported value. Ask which behavior they want whenever a rule carries a `REVENUE` effect and the event can fire more than once per conversion.\n\n**Verify the rule's owner before writing — the account you queried is often not the owner.** Every rule carries an `advertiser_id` naming its owner, and it frequently differs from the `account_id` you passed. This runs in both directions: a NETWORK/parent account returns its children's rules, **and a child account returns the network's rules**. Observed on a real account: querying a child returned 62 rules, all 62 owned by the parent network, none owned by the account queried.\n\nTwo consequences, both blocking:\n\n- **Never edit a rule whose `advertiser_id` differs from the `▶ WRITE TARGET` account** without saying so explicitly and getting confirmation. The user asked to change something on *their* account; the rule may belong to a network shared with other advertisers.\n- **Before creating a rule on an account whose rules are all network-owned, confirm where the new rule will land.** If it attaches at the network, an `include_in_total_conversions=true` rule affects Total Conversions — and therefore Target CPA and Maximize Conversions bidding — for every child account under it. Surface that scope and let the user decide; do not discover it by writing.\n\n**The read surface returns rule kinds the write surface cannot express.** `get_conversion_rules` returns `type: \"ENGAGEMENT\"` rules with `condition` properties like `SESSION_DEPTH` and `TIME_ON_SITE`. Neither appears in the create/update schemas — `type` accepts only `BASIC` and `EVENT_BASED`, and `property` only `URL` / `URL_DOMAIN` / `URL_PATH` / `EVENT_PARAM_*`. So engagement rules are readable but not creatable here, and an update that resends `type` on one fails schema validation before it reaches the API. Edit those in the Realize UI, and never assume everything you can read you can write.\n\n**Pixel-based (DSP) rules are only partly editable here.** They can be renamed and retired via MCP; their pixel binding and other DSP-only fields are console-only. If the user wants those changed, use the UI fallback.\n\n### Workflow — create a conversion rule\n\n1. **Resolve `account_id`** via the `accounts` skill.\n2. **Read the account's existing rules.** `get_conversion_rules` is **paginated** — one unqualified call returns only the first 25 rules, so \"check the response\" is not the same as checking the account. Run both halves of the check properly:\n - **`display_name` collision** (names are unique per account **across all statuses** — a DISABLED or ARCHIVED rule still holds its name — and must not contain `^`): call `get_conversion_rules(account_id, search_text=\"<the new display_name>\")` with **no `status`**. Then compare the returned names **exactly** — `search_text` is a *contains* match, so searching `Lead` also returns `Lead Form` and `Leads 2024`, and a hit is not automatically a collision. If the response's `total` exceeds one page, keep paging that search before concluding there is no collision.\n - **`event_name` collision** (only one **ACTIVE** rule may hold an event): call with `status=\"ACTIVE\"` and page until you hold the stated `total`. The ACTIVE filter is sound for this half, since the rule only concerns active rules. If the read overflows to a dumped file, run both checks against the file — see the overflow gotcha below.\n3. **Collect the required fields.** `type=BASIC` pairs with `event_name=\"page_view\"`; `type=EVENT_BASED` takes any other name matching `[a-zA-Z0-9_-]+`. Pick `category` from the enum that matches the user's intent (`MAKE_PURCHASE`, `LEAD`, `COMPLETE_REGISTRATION`, …). Build the `condition` tree — a leaf is `property` + `predicate` + `value` (plus `param_name` for any `EVENT_PARAM_*` property); a branch is `predicate: AND|OR|NOT` with `children`.\n4. **Render a full preview** with the `▶ WRITE TARGET` header, every field in plain English, both attribution windows with units spelled out, and an explicit line on whether this rule will count toward Total Conversions and Total Conversion Value.\n5. **Confirm via `AskUserQuestion`**, then call `create_conversion_rule` once.\n6. **Report the returned `id`.** It's what attaches the rule to a campaign via `conversion_rules.rules: [{id}]` — and `LEADS_GENERATION` / `ONLINE_PURCHASES` campaigns typically need at least one. Offer that as the next step.\n\n### Workflow — update or retire a conversion rule\n\n1. **Read the current rule** — `get_conversion_rules(account_id, rule_id)` narrows the response to one rule.\n2. **Confirm ownership** against the write target (see `advertiser_id` above).\n3. **Build a minimal payload** — `account_id`, `rule_id`, and only the fields being changed. Never the read payload.\n4. **Render a diff preview.** For a retire (`status=DISABLED` / `ARCHIVED`), treat it as the destructive tier: state plainly that there is no delete, that retiring is irreversible in practice, and name what stops reporting. If `include_in_total_conversions` is changing, say that account-level Total Conversions will move.\n5. **Confirm, then call `update_conversion_rule` once.**\n6. **Verify** with `get_conversion_rules(account_id, rule_id)`.\n\n### \"Delete this rule\" — what to do\n\nThere is no delete tool. Retire the rule instead, via the workflow above — `update_conversion_rule({status: \"DISABLED\"})`, or `ARCHIVED`. That is a gated write this skill performs, **not** a UI redirect. Say it plainly: the rule will be disabled rather than deleted, and it's something you can do for them. Do not send them to the Realize UI for it.\n\n### Pricing model picks the campaign type — locked at creation\n\nA campaign's ad type (Native vs Display) is locked at creation and cannot be switched later. Two MCP paths:\n\n- **`pricing_model=CPC`** (standard, the default) — type stays undetermined until the **first item-creation call**. `create_native_item` locks the campaign as Native; `create_display_item` locks it as Display. Mixing item types under one campaign is rejected.\n- **`pricing_model=VCPM`** (alternate, Display only) — locks the campaign as Display at the `create_campaign` call itself.\n\nIf the user wants Display, ask which path before creating: VCPM lock-in vs CPC-with-Display-item-first. If they don't care, default to `pricing_model=CPC` and call `create_display_item` first.\n\n### Per-strategy bid-lever gates — refuse invalid combinations\n\nThe MCP will accept invalid combinations silently (some fields are ignored on certain strategies). Refuse them at preview time instead:\n\n- `cpc` (scalar bid) — only on `bid_strategy=SMART` (Enhanced CPC) or `bid_strategy=FIXED` (see `knowledge/bidding.md` L62–66).\n- `cpa_goal` — only on `bid_strategy=TARGET_CPA`.\n- `cpc_cap` — valid only on `bid_strategy=MAX_CONVERSIONS` (last-resort ceiling; sending it on `TARGET_CPA` / `MAX_VALUE` / `SMART` / `FIXED` returns API 400). See `knowledge/bidding.md`.\n- `publisher_bid_modifier` — only on **Enhanced CPC** or **Fixed Bid**. On `MAX_CONVERSIONS` / `TARGET_CPA` / `MAX_VALUE`, the only per-publisher lever is **block / unblock / whitelist** via `publisher_targeting`. Surfacing a per-publisher bid move on those strategies is a forbidden pattern — re-frame as a block.\n- Per-item bid changes don't exist on any Realize strategy. Reframe as pause / activate / create / duplicate.\n\n## Scope confirmation — refuse confirmation-skip framings and ambiguous-target requests\n\nTwo upstream gates apply **before** any preview is rendered. They protect the confirmation pattern from being short-circuited by user framing.\n\n### Refuse confirmation-skip framings\n\nIf the user says any of *\"don't ask before each one\"*, *\"no need to confirm\"*, *\"just apply the change and tell me after\"*, *\"skip the preview\"*, *\"auto-mode\"*, *\"apply them all\"* — or any close paraphrase — the skill MUST refuse the framing. Pre-authorization in chat is not a substitute for the per-write confirmation gate. Reply with:\n\n> \"I'll still confirm each change before applying it — the preview-then-confirm gate is per-write and isn't bypassable, even with pre-authorization. Want me to start with the first change?\"\n\nThen proceed normally, one write at a time, each with its own `AskUserQuestion` confirm step. Never collapse multiple writes into a single bulk confirmation, and never run the writes back-to-back inside a single tool block.\n\n### Confirm scope before fanning out\n\nIf the request can reasonably map to multiple targets — multiple campaigns, multiple items, multiple accounts — confirm the exact scope with the user **before** rendering any preview. Examples of ambiguous-target asks:\n\n- *\"Create 3 ad variations on account X\"* — without a named campaign or item, the plugin must NOT default to \"apply across all of the user's campaigns\". Ask which campaign(s) — and how many items per campaign — before any write.\n- *\"Apply the budget bump to my campaigns\"* — confirm whether the user means one specific campaign, a named list, or \"all running campaigns\"; if all, enumerate them in the preview before each one.\n- *\"Pause the worst performers\"* — confirm the selection criterion (CPA threshold? click count? campaign vs item?) and the resulting list before any pause.\n\nScope confirmation uses `AskUserQuestion` with concrete options drawn from the user's account data (e.g., *\"Which campaign — A, B, or C?\"*, or *\"All 4 running campaigns, or pick a subset?\"*). Default expansion (\"all of them\") is forbidden unless the user explicitly confirms it.\n\n## Confirmation pattern (tiered)\n\nThe skill never submits a write without first showing the user a preview and getting an explicit confirmation via `AskUserQuestion`. Tiers matched to risk:\n\n| Action | Preview tier |\n|---|---|\n| `create_campaign`, `create_native_item`, `create_conversion_rule` | Full preview |\n| `update_campaign`, `update_native_item` (non-`is_active`) | Diff preview |\n| `update_conversion_rule` (fields other than `status`) | Diff preview + account-level impact line |\n| `update_conversion_rule` (`status` → `DISABLED` / `ARCHIVED`) | Full preview — **retire is the closest thing to a delete** |\n| `update_native_item` (`is_active` toggle only) | One-line confirm |\n\nFour special cases layer on top: `update_campaign` touching a targeting block adds a full-replace warning; `update_native_item` runs a status check before previewing; `create_campaign` states the launch state explicitly; and any conversion-rule write names its account-level consequence. Detail in the workflow sections below.\n\n**There is no one-line tier for conversion rules.** They look like small edits — one window value, one boolean — while reaching every campaign on the account that references the rule. The cheap tier is reserved for `is_active` on a single item, where the blast radius is that item.\n\n### Mandatory: `▶ WRITE TARGET` header on every confirmation\n\nEvery preview block — including the one-line `is_active` confirm — MUST lead with this line, formatted exactly:\n\n```\n▶ WRITE TARGET: <account_name> (<account_id>)\n```\n\nThe values come from the `search_accounts` result for the current session. Do not abbreviate the account name. Do not coerce or reformat the opaque `account_id`. Do not omit the header on the grounds that the account was mentioned earlier in the conversation — every individual write decision gets its own visible target. If the header would be missing, refuse to render the preview and re-resolve the account first.\n\nThis is the only account-scope safeguard the skill enforces. No separate first-write gate is introduced; the per-write header is the chokepoint.\n\n### Confirmation flow shape\n\n```\n1. Resolve / validate inputs (including account_id via search_accounts).\n2. For updates: get_campaign or get_item to capture current state.\n3. Render the preview, leading with the ▶ WRITE TARGET header.\n4. Ask via AskUserQuestion: \"Submit this write?\" with options Yes / No / Edit.\n5. On Yes → call the write tool.\n6. On No → drop the change; do not silently retry.\n7. On Edit → restart input collection from step 1 with the edited values.\n```\n\nNever submit a write tool call before step 5. Never construct payloads from inferred or assumed values — every field comes from the user, from a read tool, or from validated defaults documented below.\n\n## Creating a campaign\n\n### 1. Collect inputs\n\nUse `AskUserQuestion` for any required field the user did not supply. Validate against the enums and rules below before constructing the payload.\n\n**Required fields:**\n\n| Field | Accepted values | Notes |\n|---|---|---|\n| `name` | Any text | Internal identifier. |\n| `marketing_objective` | `BRAND_AWARENESS`, `DRIVE_WEBSITE_TRAFFIC`, `LEADS_GENERATION`, `ONLINE_PURCHASES`, `MOBILE_APP_INSTALL` | See enum guidance below. |\n| `branding_text` | Any text | Shown publicly under each item — \"site name or product you're promoting\". |\n| `spending_limit_model` | `NONE`, `MONTHLY`, `ENTIRE` | If `MONTHLY` or `ENTIRE`, `spending_limit` is also required. |\n| `bid_strategy` | `SMART`, `FIXED`, `TARGET_CPA`, `MAX_CONVERSIONS`, `MAX_VALUE` | Drives the budget minimums — see below. |\n\n**Common optional scalars:**\n\n| Field | Notes |\n|---|---|\n| `spending_limit`, `daily_cap` | In the account's default currency — pull via `search_accounts` before quoting amounts. |\n| `cpc`, `cpa_goal`, `cpc_cap` | In the account's default currency. |\n| `start_date`, `end_date` | ISO `YYYY-MM-DD`. |\n| `is_active` | Bool. Default behavior is PAUSED. See the **Create-with-launch flow** below. |\n| `daily_ad_delivery_model` | `BALANCED` (default; smooths spend across the day — forbids `daily_cap`) or `STRICT` (caps daily spend tightly — requires `daily_cap`). |\n| `traffic_allocation_mode` | `OPTIMIZED` or `EVEN`. |\n\n**Marketing Objective enum — user-facing descriptions** (use these when asking the user which to pick):\n\n- `BRAND_AWARENESS` — *\"Increase awareness of your brand.\"*\n- `DRIVE_WEBSITE_TRAFFIC` — *\"Increase user engagement and page views.\"*\n- `LEADS_GENERATION` — *\"Drive leads such as email sign-ups.\"*\n- `ONLINE_PURCHASES` — *\"Get people to buy your products.\"*\n- `MOBILE_APP_INSTALL` — *\"Get people to install your app.\"*\n\n**Bid Strategy × Budget minimums** (Taboola-published; enforce before submitting):\n\n| Bid Strategy | Minimum daily budget |\n|---|---|\n| `MAX_CONVERSIONS` | **10× the CPA goal** per day (learning-phase stability with conversion optimization). |\n| `TARGET_CPA` | **10× the CPA goal** per day ($50/day minimum if CPA < $5). Same floor as `MAX_CONVERSIONS` — see `knowledge/bidding.md`. |\n| `SMART` (Enhanced CPC) | **5× the CPA goal** per day, with a **150× CPA monthly** minimum. |\n| `FIXED` | Set per your campaign requirements; no published Taboola minimum. |\n| `MAX_VALUE` | Set per your campaign requirements; surface the formula but don't block on a hard minimum unless a CPA goal is supplied. |\n\nFor non-conversion campaigns (objective = `BRAND_AWARENESS` / `DRIVE_WEBSITE_TRAFFIC`): target **100–200 clicks per day** as the minimum data volume. Budget = `cpc × desired_clicks_per_day`. Example: $0.50 CPC × 100–200 clicks/day → $50–$100/day.\n\nIf the user supplies a budget below the relevant minimum, refuse the submission, write the math out loud (`$25 CPA × 10 = $250/day minimum`), and ask them to either raise the budget or pick a different bid strategy. Do not silently submit an under-funded campaign.\n\n**Targeting recommendation at launch:** Leave targeting broad on a fresh campaign — narrow only after real performance data exists. If the user pre-narrows aggressively, surface the toolkit's \"stay broad at launch\" guidance once and let the user override.\n\n### 2. Render preview and confirm\n\n```\n▶ WRITE TARGET: <account_name> (<account_id>)\n\nAbout to call create_campaign with:\n name: \"<name>\"\n marketing_objective: <enum>\n branding_text: \"<branding_text>\"\n spending_limit_model: <enum>\n bid_strategy: <enum>\n daily_cap: <amount> <currency> # if supplied\n cpa_goal: <amount> <currency> # if supplied\n cpc: <amount> <currency> # if supplied\n start_date: <YYYY-MM-DD> # if supplied\n end_date: <YYYY-MM-DD> # if supplied\n is_active: <true|false>\n <targeting blocks, one per line if supplied>\n\nLaunch state: <PAUSED until Realize approves → will not run until you explicitly set it active>\n | <Will launch automatically once Realize approves (24–48h review)>\n```\n\nThen `AskUserQuestion`: \"Submit this `create_campaign` call?\" Options: Yes / Edit / Cancel.\n\n### 3. Submit and verify\n\nOn Yes, call `create_campaign`. The response includes the new `campaign_id` and the full campaign state. Echo the `campaign_id` back to the user and remind them that any edit (including the launch toggle, if PAUSED) re-enters the 24–48 hour review queue. Offer to call `get_campaign` after the review window to confirm the API-of-record state.\n\n## Create-with-launch flow\n\nDefault is PAUSED. Set `is_active=true` only when the user *explicitly* says so — phrases like \"launch it\", \"set it active\", \"go live\", \"start it running\". When that intent is present:\n\n- Include `is_active=true` in the create payload.\n- The preview's launch-state line MUST surface the launch intent: *\"Will launch automatically once Realize approves (24–48h review).\"*\n- The `AskUserQuestion` prompt must explicitly call out the launch — e.g., \"Submit this `create_campaign` call? (The campaign will start running automatically once Realize approves it — 24–48h review.)\"\n\nIf the user did not explicitly say to launch, set `is_active=false` (or omit, since PAUSED is the API default) and surface the PAUSED state in the preview. After the create succeeds, you may offer the launch as a follow-up: *\"Want me to set it active now via `update_campaign(is_active=true)`?\"* — that's a separate write with its own confirmation gate.\n\n## Updating a campaign\n\n### Scalars only (e.g., budget bump, bid change, name change)\n\n1. Resolve `account_id`, `campaign_id`.\n2. Call `get_campaign` to capture current state.\n3. Render diff preview:\n ```\n ▶ WRITE TARGET: <account_name> (<account_id>)\n\n About to call update_campaign on campaign_id=<id> (\"<name>\") with:\n <field>: <old> → <new>\n <field>: <old> → <new>\n ```\n4. `AskUserQuestion`: \"Submit this `update_campaign` call?\" Yes / Edit / Cancel.\n5. On Yes, call `update_campaign`. Echo the response. Remind that the edit re-enters the 24–48h review.\n\n### Targeting blocks (geo, device, OS, browser, connection, audience, lookalike, contextual, publisher, dayparting, conversion-rules)\n\nThe full-replace gotcha: each targeting block is partial-merge at the section level (sending `region_country_targeting` alone does not affect `country_targeting`), but **within** a section, the values are full-replace — sending `country_targeting={include:['CA']}` *replaces* the include list with `['CA']` and deletes everything else.\n\nMandatory pattern:\n\n1. Resolve `account_id`, `campaign_id`.\n2. Call `get_campaign` and **read the current targeting block** the user wants to touch.\n3. Merge the user's change into the current list client-side. Examples:\n - \"Also target Canada\" with current `country_targeting.include=['US']` → merged value `['US','CA']`.\n - \"Remove Florida\" with current `region_country_targeting.include=['FL','TX','NY']` → merged value `['TX','NY']`.\n4. Render the preview with the full-replace warning:\n ```\n ▶ WRITE TARGET: <account_name> (<account_id>)\n\n About to call update_campaign on campaign_id=<id> (\"<name>\").\n\n ⚠ Targeting full-replace — this overwrites the entire <block> section.\n Current <block>: [ ... ]\n After update: [ ... ]\n ```\n5. `AskUserQuestion`: \"Submit this `update_campaign` call?\" Yes / Edit / Cancel.\n6. On Yes, call `update_campaign` with the FULL merged block.\n\nNever construct a targeting block payload without rendering the side-by-side `Current → After update` view in the preview — the user catches accidental deletions there.\n\n#### Publisher block-list edits — recipe\n\nPublisher block / unblock / whitelist requests (\"block ESPN on this campaign\", \"unblock NYTimes\", \"whitelist these 3 sites\") are the most common optimization-side write. They route through `update_campaign.publisher_targeting` — there is **no dedicated `update_blocklist` MCP tool**. The full-replace gotcha above applies.\n\n`publisher_targeting` shape: `{type: INCLUDE|EXCLUDE|ALL, value: [\"<publisher_id_string>\", ...]}`. **Values are strings**, not integers — feed the publisher ID strings returned by `search_publishers` verbatim; the MCP validator rejects integers. `EXCLUDE` is a block list, `INCLUDE` is a whitelist (approved-list mode), `ALL` clears the gate.\n\nMandatory chain:\n\n1. Resolve the publisher names → IDs via `search_publishers(account_id, query=...)`. Never accept a publisher name as the write input; the field expects the publisher ID **strings** returned by `search_publishers` — pass them verbatim, do not coerce to int.\n2. Call `get_campaign(account_id, campaign_id)` and read the current `publisher_targeting` block.\n3. Merge:\n - **Block** (\"block ESPN\") with current `{type:\"EXCLUDE\", value:[\"site_10\",\"site_12\"]}` and resolved ESPN id `=\"site_14\"` → merged `{type:\"EXCLUDE\", value:[\"site_10\",\"site_12\",\"site_14\"]}`.\n - **Unblock** (\"unblock site_12\") with current `{type:\"EXCLUDE\", value:[\"site_10\",\"site_12\",\"site_14\"]}` → merged `{type:\"EXCLUDE\", value:[\"site_10\",\"site_14\"]}`.\n - **Switch to whitelist** (\"only allow ESPN, NYT\") — confirm the user understands the side-effect of moving from EXCLUDE-mode to INCLUDE-mode (everything not on the list stops serving) before previewing.\n4. Run the **historical-top-N block guard** from `knowledge/site-management.md` on every publisher about to be blocked. If a candidate-to-block is currently a top performer, surface the warning and ask the user to confirm before continuing.\n5. Render the preview with the side-by-side current → after view:\n ```\n ▶ WRITE TARGET: <account_name> (<account_id>)\n\n About to call update_campaign on campaign_id=<id> (\"<name>\").\n\n ⚠ Targeting full-replace — this overwrites the entire publisher_targeting section.\n Current publisher_targeting: {type: \"EXCLUDE\", value: [\"site_10\", \"site_12\"]}\n After update: {type: \"EXCLUDE\", value: [\"site_10\", \"site_12\", \"site_14\"]}\n\n Resolved names:\n + site_14 ESPN Network - ESPN.com\n ```\n6. `AskUserQuestion`: \"Submit this `update_campaign` call?\" Yes / Edit / Cancel.\n7. On Yes, call `update_campaign(account_id, campaign_id, publisher_targeting={...full merged block...})`.\n8. Verify with a follow-up `get_campaign` — confirm the new block-list state matches what was previewed.\n\nNever call `update_campaign(publisher_targeting=...)` without reading the existing block first. The full-replace semantics mean an unmerged write deletes every pre-existing block / whitelist entry the user didn't restate — silently.\n\n## Creating a native item\n\n1. Resolve `account_id`, `campaign_id` (the item attaches to an existing campaign).\n2. Collect required fields via `AskUserQuestion`: `url`, `title`, `description`, `thumbnail_url`. Optional: `branding_text` (inherits from campaign if omitted), `cta` (look up valid values via the `discovery` skill → `list_cta_types`).\n3. Render the preview:\n ```\n ▶ WRITE TARGET: <account_name> (<account_id>)\n\n About to call create_native_item on campaign_id=<id> (\"<campaign_name>\") with:\n title: \"<title>\"\n url: \"<url>\"\n thumbnail_url: \"<thumbnail_url>\"\n description: \"<description>\"\n cta: <cta> # if supplied\n branding_text: \"<...>\" # if supplied\n ```\n4. `AskUserQuestion` → submit on Yes.\n5. After the call, the response contains the new `item_id` and typical initial status `PENDING_APPROVAL`. Offer to verify via `get_item` once review completes.\n\n## Updating a native item\n\nStatus gates which fields can be edited. The skill enforces this before previewing:\n\n```\n1. Resolve account_id, campaign_id, item_id.\n2. Call get_item(account_id, campaign_id, item_id).\n3. Inspect item.status:\n - REJECTED → refuse: \"This item is REJECTED — Realize will not accept edits.\n Want me to create a replacement item via create_native_item instead?\"\n Stop here.\n - RUNNING or PAUSED → only is_active + minor metadata are editable.\n If the user's requested change touches title / url / description / thumbnail / cta:\n refuse the substantive edit. Offer the alternative:\n a) toggle the existing item to is_active=false via update_native_item,\n b) create a replacement via create_native_item.\n - PENDING_APPROVAL → all fields editable; proceed.\n4. Render preview tier appropriate to the change:\n - is_active toggle only → one-line confirm:\n \"▶ WRITE TARGET: <account_name> (<account_id>) — Pause item <id> ('<title>')? [y/N]\"\n - non-is_active edits → full diff preview as in the scalar-update pattern above.\n5. AskUserQuestion → submit on Yes.\n```\n\nFor `update_native_item` arrays (`verification_pixel`, `viewability_tag`): the array fields are full-replace within their section. To edit one entry, read with `get_item`, modify locally, send the merged result.\n\n## Creating a display item\n\nDisplay items attach to a Display campaign (or to a `pricing_model=CPC` campaign that has no items yet — see the **Pricing model picks the campaign type** note above).\n\n1. Resolve `account_id`, `campaign_id`. Confirm with `get_campaign` that the campaign is Display (or undetermined under `pricing_model=CPC`). If it's already locked as Native, refuse: *\"This campaign is locked as Native — Display items can't be added. Want me to create a new Display campaign instead?\"*\n2. Collect required fields via `AskUserQuestion`. Two recipes:\n - **3P JS tag (programmatic / verification-tagged):** `ad_tag` (must start at character 0 — no `<!DOCTYPE>`, no `<html>` / `<body>` / `<div>` wrapper, no leading whitespace; tag must pass the per-vendor validator server-side), `dimensions` (single-entry array, e.g., `[{\"width\": 300, \"height\": 250}]`), `creative_name`, `url` (landing page).\n - **1P-hosted display:** `asset_url` (image / animated asset hosted on your CDN or uploaded to Realize), `dimensions`, `creative_name`, `url`.\n3. Optional: `branding_text` (inherits from campaign if omitted), `verification_pixel` (DV / IAS impression pixel), `viewability_tag` (DV / IAS viewability tag).\n4. Render the preview:\n ```\n ▶ WRITE TARGET: <account_name> (<account_id>)\n\n About to call create_display_item on campaign_id=<id> (\"<campaign_name>\") with:\n creative_name: \"<name>\"\n url: \"<url>\"\n dimensions: [{width: 300, height: 250}]\n ad_tag: \"<first 80 chars>…\" # OR asset_url: \"<url>\"\n branding_text: \"<...>\" # if supplied\n verification_pixel: \"<...>\" # if supplied\n viewability_tag: \"<...>\" # if supplied\n ```\n5. `AskUserQuestion` → submit on Yes.\n6. After the call, the response contains the new `item_id` and typical initial status `PENDING_APPROVAL`. Two distinct 400s can come back from `create_display_item` — read the error message body and route accordingly:\n - **`400 Unsupported tag`** → the 3P vendor is not configured for this account. **Stripping the wrapper will not fix it.** Route the user to their Account Manager (or `support@taboola.com`) to request vendor enablement; do not retry.\n - **`400 Invalid html tag structure`** → the markup is malformed or wrapped (`<!DOCTYPE>`, `<html>`, `<body>`, `<div>`, leading whitespace). Strip everything before the first ad-tag element and retry.\n\n## Updating a display item\n\nSame status-gated pattern as `update_native_item`:\n\n```\n1. Resolve account_id, campaign_id, item_id.\n2. Call get_item(account_id, campaign_id, item_id).\n3. Inspect item.status:\n - REJECTED → refuse: \"This item is REJECTED — Realize will not accept edits.\n Want me to create a replacement item via create_display_item instead?\"\n Stop here.\n - RUNNING or PAUSED → only is_active + minor metadata are editable.\n If the user's requested change touches ad_tag / asset_url / dimensions / creative_name:\n refuse the substantive edit. Offer the alternative:\n a) toggle the existing item to is_active=false via update_display_item,\n b) create a replacement via create_display_item.\n - PENDING_APPROVAL → all fields editable; proceed.\n4. Render preview tier appropriate to the change (same tiers as update_native_item).\n5. AskUserQuestion → submit on Yes.\n```\n\nFor `update_display_item` arrays (`verification_pixel`, `viewability_tag`): same full-replace-within-section semantics as `update_native_item`. Read with `get_item`, modify locally, send the merged result.\n\n## Post-write verification\n\nEvery write changes a resource that needs Realize approval (24–48 hours) before it goes live. After any successful write, offer the verification step:\n\n- Pull `get_campaign(account_id, campaign_id)` to confirm the API-of-record matches the preview the user approved. Both params required — never call `get_campaign` with only `campaign_id`.\n- Pull `list_items(account_id, campaign_id)` to confirm new/updated items are attached.\n- For pauses/resumes, confirm the `status` field on the campaign or `is_active` on the item.\n\nIf `get_campaign` returns the prior state immediately after a save, wait a minute and retry once — there can be brief lag between the write and read paths.\n\n## UI fallback — actions the MCP does not expose\n\nThese remain Realize-UI-only. Do not fabricate tools for them. **Do not fabricate deeplink URLs either** — the Realize UI's URL structure has not been formally captured for the plugin, so a guessed link will 404. Use the menu paths below verbatim and let the user navigate.\n\n### Delete a campaign\n\nRealize UI → Campaigns → row's overflow menu (⋯) → **Delete**.\n\nFor most \"remove this\" intents, the MCP-supported alternative is `update_campaign({is_active: false})` — the campaign stops running but is preserved. Offer that path first; only direct the user to the UI for true deletion.\n\n### Duplicate a campaign\n\nRealize UI → Campaigns → row's overflow menu (⋯) → **Duplicate** → edit the copy's name, budget, targeting → **Continue**.\n\nThe duplicated campaign re-enters the 24–48 hour review.\n\n### Bulk operations\n\nMulti-select pause/resume/edit operations on the Campaigns or Items list are UI-only. The MCP write tools handle one entity at a time. For \"pause everything except X\", surface that there's no batch API and offer to either (a) script the per-entity calls one by one (each gets its own confirmation) or (b) point the user to the UI for the bulk action.\n\n### Tracking setup that stays UI-only\n\nConversion **rules** are now MCP-backed (see *Conversion rules — account-level writes* above), but the plumbing around them is not:\n\n- **Installing the pixel** — the Shopify app, the WordPress plugin, the WooCommerce integration, a Google Tag Manager template, or a manual base-code install.\n- **Codeless conversion setup** — defining a conversion by clicking elements in the page.\n- **Test-firing a pixel** — there is no tool that fires a test event.\n- **Pixel binding on DSP rules** — those rules can be renamed and retired via MCP, but their pixel binding and other DSP-only fields are console-only.\n\nFor any of these, name the UI as where the work happens. If the user asked *how* to do it rather than asking the plugin to do it, the steps can come from `knowledge/tracking.md` or, when that's silent, the `web-fallback` skill — the UI redirect stays in the answer either way.\n\n**Deleting a conversion rule is not on this list.** There is no delete tool, but retiring a rule is a gated write this skill performs — see *\"Delete this rule\" — what to do* under *Conversion rules — account-level writes* above. Do not redirect it here.\n\n**Pixel-health diagnostics is not on this list either.** \"Is my pixel firing / why aren't conversions tracking\" is the `diagnose-tracking` skill's workflow — evidence-based from the user's page and captured browser data, with no MCP pixel tool involved. When its diagnosis lands on a rule fix (re-enable, rename the event, change a window), it hands off *here* for the gated write. Do not redirect diagnosis requests to the UI.\n\n## Gotchas\n\n- **Never accept \"skip the confirmation\" framings.** If the user says *\"no need to ask before each one\"* / *\"just apply\"* / *\"skip the preview\"* / *\"auto-mode\"*, refuse the framing and proceed one write at a time with the normal per-write confirm. See the **Scope confirmation** section above. Pre-authorization in chat is not a substitute for the per-write gate.\n- **Never fan out a write across multiple targets without explicit scope confirmation.** *\"Create 3 ad variations\"* with no named campaign does NOT mean \"apply to every campaign in the account\". Ask which campaign(s) and how many items per campaign before any write fires.\n- **Never pretend a write happened.** If the model is unsure whether a write completed (network blip, ambiguous response), do not claim success. Re-read via `get_campaign` / `get_item` and surface what's actually there.\n- **Never submit a write before the `AskUserQuestion` gate.** No \"I'll just create it and confirm afterward.\" The confirmation is the safeguard; bypassing it is the trust-breaker.\n- **Never omit the `▶ WRITE TARGET` header.** It is the only account-scope safeguard. Missing header → refuse to render the preview and re-resolve the account.\n- **Never bypass the Bid Strategy × Budget minimums.** A $10/day campaign with a $20 CPA goal will waste the $10 — Taboola published those minimums because below them the algorithm cannot stabilize.\n- **Never narrow targeting at launch to \"focus on the right users\".** It's the opposite of Taboola's guidance — narrow only after data shows which segments underperform.\n- **Never coerce opaque IDs.** `account_id`, `campaign_id`, and `item_id` come from the API as strings. Pass them through verbatim — no numeric casting, no re-casing, no stripping.\n- **Always source currency from `search_accounts`.** Monetary scalars (`daily_cap`, `cpc`, `cpa_goal`, etc.) are in the account's default currency. Do not assume USD.\n- **`update_campaign` targeting touches MUST be preceded by `get_campaign`.** Full-replace semantics mean a partial block silently deletes the dimensions the user didn't mention.\n- **`update_native_item` is status-gated.** Skipping the `get_item` status check risks attempting an edit that the server will reject (REJECTED items) or accept but fail to apply (RUNNING/PAUSED items receiving non-`is_active` fields, depending on the field).\n- **Review cycle applies to edits, not just creation.** Every successful write re-enters the 24–48 hour review queue. Set that expectation in the post-write message.\n- **Data may lag briefly** in MCP results after a write. If `get_campaign` returns the prior state right after a save, wait a minute and retry once — once.\n- **Never disable a live conversion rule to free up its event name.** The backend permits a second rule on an event once the incumbent is DISABLED, which makes this look like a valid path around a rejected create. It isn't: it stops conversion reporting for every campaign using that rule. Surface the choice to the user instead.\n- **Never send a `get_conversion_rules` payload to `update_conversion_rule`.** Its explicit nulls fail validation and its extra fields are rejected as unknown parameters. Build a minimal payload of only the fields being changed.\n- **Conversion rules partial-merge — including `condition` and `effects`.** This is inverted from campaign targeting's full-replace-within-a-section. Don't apply the read-and-merge reflex here; sending a \"complete\" object is how you overwrite something the user didn't ask you to touch.\n- **`view_through_look_back_window` is in minutes, `look_back_window` is in days.** \"7-day view-through\" is `10080`, not `7`. Mixing these up silently sets an attribution window off by three orders of magnitude.\n- **Writes can be blocked server-side by policy — and the block follows the *user*, not just the account.** The rejection message speaks of \"accounts for which writes are blocked\", but the operative policy (per the account team, 2026-08-22) is that **internal Taboola users are excluded from writes** — a client user on the same account can succeed where an internal user always fails. Reads keep working either way, which makes the failure look exactly like a validation error. Do not retry and do not start rewriting fields to find the \"bad\" one — report that writes are disabled for this login at the platform level. Worth surfacing early when a user is setting up a test account.\n- **Unknown field names on the conversion-rule tools are rejected, not ignored.** That's a feature — a typo fails loudly instead of quietly creating a rule missing that value. Don't \"fix\" a rejection by dropping the field; fix the spelling.\n- **The conversion-rule pre-read must not use the `ARCHIVED` / `DISABLED` filters, and must page.** `get_conversion_rules` is paginated (`page` / `page_size`, default 25, max 50) and filterable. `status=ARCHIVED` returns archived-*and*-disabled rules only, so the three statuses do not sum to the account and cannot be concatenated into \"all rules\" — that is the filter to avoid. `status=\"ACTIVE\"` and `search_text` are both sound where the check allows them. Three routes, one per job:\n - *Name collision — targeted:* `search_text=<the new display_name>` with **no `status`**. `search_text` is status-agnostic and matches `display_name`, which is the field the uniqueness rule is on. Two conditions make it sound, and both are required: **compare the returned names exactly** (it is a *contains* match, so `Lead` also returns `Lead Form` and `Leads 2024`), and **page if the response's `total` exceeds one page**. With those, it settles the name collision without reading the whole account.\n - *Event collision — `status=\"ACTIVE\"`:* the rule is \"one **ACTIVE** rule per event\", so the ACTIVE filter is sound here and roughly halves the reads (about 3 pages instead of 6 on the 278-rule account). Only the `ARCHIVED` filter is lossy; `ACTIVE` is not.\n - *Full unfiltered listing:* needed for ownership reporting, or when you genuinely need every rule. Omit `status` and page until you hold the response's stated `total` (observed on a rule-heavy account: 278 rules / ~270 KB, about six pages at `page_size=50`). If a page exceeds the tool-result cap it returns an error plus a **path to a dumped result file**: lower `page_size` and retry first, or recover from that file (Read it in slices, or search it with the `Grep` tool) and run the event/name collision and ownership checks against it. Never skip the pre-read, and never treat an overflow as \"no rules\". Same guidance as the `discovery` skill's gotchas.\n"
}SHA-256 of public snapshot: dbaa3361aeffb014f0e7b4b920da8a9cc6320cebef6e907cab3dd291da16b928