MGID Auto-Optimization Simulator
Orchestrator only: gate → collect signals → merge → rank → preview. Does not reimplement signal rules — load and execute each signal skill's playbook.
Tools: tool reference
Schemas: schemas
Merge/rank: reference.md
Signal skills (load, do not duplicate their rules)
| Skill | Output |
|---|---|
mgid-tracking-health-monitor |
Gate pass/fail |
mgid-budget-reallocator |
reallocate_budget |
mgid-source-auditor |
block / adjust_qf |
mgid-creative-fatigue-detector |
pause_teaser |
mgid-campaign-launch-builder — separate unless user asks setup + optimize.
Required inputs
| Input | Required | Default |
|---|---|---|
| Scope | Yes | see Campaign scope below |
| Date range | Yes | last 7 days; baseline = prior 7d |
| Goal / target | Yes | user or get_campaign → bidding.price |
| Mode | Yes | simulation |
No get_account_goals. Target CPA must come from user or campaign config.
Campaign scope
Default: analyze all active campaigns up to 25 (DEFAULT_CAMPAIGN_SCOPE).
| Active count | Behavior |
|---|---|
| ≤ 25 | Analyze every active campaign — no silent truncation |
| > 25 | Stop and ask the user before Phase 2 (see prompt below) |
User may override scope at any time:
- Named campaign IDs → analyze only those (still run gate on all active)
- “All active” / “full account” → analyze every active campaign regardless of count
- “Top N by spend” → analyze top N only; state N and the cutoff spend in chat
Never analyze only 1–2 campaigns unless the user explicitly scoped that way.
When active count > 25, ask:
This account has {N} active campaigns. Default scope covers up to 25.
How should I scope the simulation?
1. Top 25 by 7d spend (default — fastest)
2. All {N} active campaigns (more MCP calls, full portfolio)
3. Specific campaign IDs (you name them)
Wait for the user's choice before collecting signals. If they pick (1), rank by
get_portfolio_statistics → spent and take the top 25. Record scope metadata
in the plan file (see reference.md).
Workflow
Phase 0 — Gate
Run mgid-tracking-health-monitor playbook.
On fail → fix-first plan only; gates.tracking_health = fail.
Phase 1 — Scope campaigns
get_portfolio_statistics→ spend, conversions and CPA for every campaign in one calllist_campaigns→ filterstatus.name == active, pick uptype,limits,bidding,sourcesOptimization→ count N- Drop
search_feedandnewslettercampaigns from scope and record them as unsupported (see tool reference) - Apply Campaign scope rules (ask user if N > 25 unless user already specified scope);
"top 25 by spend" sorts on the portfolio row's
spent get_campaign_statisticsonly for campaigns whose daily rows a signal skill actually needs (budget cap detection, period-over-period)
Phase 1 costs 2 calls regardless of portfolio size. Never open with one
get_campaign_statistics per campaign.
Phase 2 — Collect signals
Execute each signal skill on every campaign in scope; collect Recommendation[].
Do not re-apply their thresholds here.
Pass the scoped campaign list to budget reallocator and source auditor — they must not silently shrink scope on their own.
Creative fatigue scoping (priority order):
- Primary: campaigns with no source/budget/tcpa recommendations in this plan
and avg daily spend < 85% of
limits.dailyLimit(underspending) - Fallback: campaigns with 7d spend ≥ $200 or top 5 by spend in scope (whichever set is smaller)
Use run_custom_report with dimension teaserId (see creative skill) when the
account has the customReports grant; fall back to get_teaser_statistics (v1) per teaser.
Record every campaign scanned in the plan file under summary.creative_fatigue.campaigns_scanned —
not only campaigns with pause recommendations.
Phase 3 — Merge
Apply conflict rules in reference.md. Dedupe; populate suppressed.
Phase 4 — Rank
Sort by (confidence × estimated_savings) / risk. Cap at 10 (ASSUMPTION).
Phase 5 — Preview
Chat: summary block + table only.
File: write optimization-plan-{plan_id}.json with full OptimizationPlan +
stats_snapshot (plain numbers — no hashing).
Ask user to approve by rank number.
Phase 6 — Apply (after approval, write scope)
Read plan file back. Re-fetch statistics; compare to stats_snapshot (ASSUMPTION:
warn if >10% drift). Call write tools per ads-mcp-tools.md.
Plan storage, audit trail, idempotent apply → ads-mcp server, not this skill.
Anomalies (simulator-only)
ASSUMPTION: flag when 7d totals vs prior 7d change ≥ 40% on CPC, CVR, or CPA.
Do not use day-over-day — native traffic is too volatile.
Emit alert_anomaly only (no auto-action).
Guardrails
- Never apply without explicit approval.
- Simulation default.
- Mark recommendations
mcp_tool_unavailablewhen write tools absent.
Example chat output
Optimization plan — SIMULATION
Plan ID: opt-2026-07-06-001
Scope: 16 / 16 active campaigns · 2026-06-29 → 2026-07-05
Goal: CPA ≤ $15.50 (from campaign 12061865)
Gates: tracking ✓
Plan file: optimization-plan-opt-2026-07-06-001.json
| # | Action | Entity | Reason | Impact | Risk |
| 1 | Block source | 242669 @ 12061865 | CPA 2.1× target | −$18–23/day | Low |
...
Do not paste full JSON into chat.
Simulator — merge, rank, preview only
Signal skills own thresholds and impact formulas. This file covers orchestration only.
Schemas: schemas
Scope default: threshold assumptions (DEFAULT_CAMPAIGN_SCOPE)
Campaign scope (orchestrator)
The simulator owns scope — signal skills analyze the campaign list they receive.
| Constant | Value |
|---|---|
DEFAULT_CAMPAIGN_SCOPE |
25 |
Selection algorithm
active = list_campaigns filtered to status=active
N = len(active)
if user gave explicit scope → use it
else if N <= 25 → scope = all active ids
else → ask user (see SKILL.md); default choice = top 25 by 7d spend
After scope is fixed, fetch get_campaign_statistics for each id in scope before
Phase 2.
Plan file scope block
Always populate:
"scope": {
"auth_id": 200316,
"client_id": 330838,
"account_id": 330838,
"total_active_campaigns": 42,
"analyzed_campaign_ids": [12061397, 12061865],
"analyzed_count": 25,
"scope_mode": "top_by_spend",
"scope_limit": 25,
"user_confirmed": true
}
scope_mode |
Meaning |
|---|---|
all_active |
Every active campaign analyzed |
top_by_spend |
Subset ranked by 7d spend (record limit) |
user_specified |
User named ids or a custom filter |
When analyzed_count < total_active_campaigns, chat summary must show
{analyzed} / {total} active campaigns and name the scope mode.
Conflict resolution
| Conflict | Resolution |
|---|---|
| Block vs budget to same campaign | Defer block unless CPA > 2× target (ASSUMPTION) and post-learning |
| Pause teaser + budget same campaign | Never pause last active teaser |
| Budget out + in same pair | Single net reallocation line |
| Multiple QF on same entity | Smallest decrease wins |
| Top converter | Defer to source-auditor downgrade rules |
| Tracking fail | Suppress all spend actions |
| CPA Tune learning | Suppress blocks — source-auditor owns detail |
Ranking
score = (confidence_score × estimated_daily_savings) / risk_score
confidence: high=3, medium=2, low=1
risk: low=1, medium=2, high=3
Take top 10 unless user asks for full list.
Apply order
When write tools available:
set_teaser_status(pause)update_quality_factorsupdate_source_filters(blocks)update_campaign(budget)
Stop on first hard failure; report partial apply in chat.
Drift check at apply
Compare fresh get_campaign_statistics totals to stats_snapshot in plan file.
ASSUMPTION: if spent or conversionsCostBuy moved >10%, warn and require
re-confirm before writes.
No sha256 — numeric compare only.
Plan file location
Write under workspace, e.g. ./optimization-plan-{plan_id}.json.
When ads-mcp adds plan storage API, prefer server-side plan id over local files.