---
name: sync-state
description: "Sync STATE.json with current campaign data from all platforms. Use when the user asks to refresh state, sync campaigns, update STATE.json from live data, or pull latest campaign snapshots. Also use when the user asks in Japanese (STATE.jsonを更新 / 最新データに同期して / キャンペーン情報を取り込み直して)."
metadata:
  version: 0.12.1
---

# Sync State

> PREREQUISITE: Read `../_mureo-shared/SKILL.md` for auth, security rules, output format, and **Tool Selection** (Read/Write on Code, `mureo_strategy_*` / `mureo_state_*` MCP on Desktop / Cowork).

Synchronize STATE.json with the current state of all marketing platforms.

## Mixed operation

Manual operation in the platform UI and mureo-driven operation coexist — most accounts run that way for a while after onboarding, and it applies to every platform. Treat any change you find that mureo did not make as a first-class fact, not an anomaly: the `action_log` records what mureo did, never the full history of the account, so never infer current state from it. When a diff surfaces a change made outside mureo, say so in the report rather than silently absorbing it into the new state.

## Prerequisites
- STRATEGY.md and STATE.json should exist in the current directory (run the `onboard` skill first if not)

## Steps

> Note: the **Diagnostic preamble** (learning insights + advisor consult) from `../_mureo-shared/SKILL.md` is intentionally **not** required here — sync-state is mechanical data synchronization with no judgement calls.

0. **Establish today**: call `mureo_state_get` **first, on every host** (including Claude Code, where you would otherwise `Read` the file) and take `server_now` from its response — ISO 8601 with UTC offset, e.g. `2026-07-28T10:12:33+09:00`. Its date is the **only source of the current date** for this sync (no shelling out — this skill must run in Bash-less headless hosts). The dates inside the document — `last_synced_at`, `reports.*.period`, `action_log` timestamps — are **history**: use them to describe what changed, never to decide what "now" is or whether a sync is still needed. **Never write `server_now` into STATE.json**; it is a response field, and a persisted copy becomes tomorrow's stale "today".

1. **Read current STATE.json** (if exists) to track changes — the same `mureo_state_get` response from step 0 on MCP hosts.

2. **Discover platforms**: Identify all platforms registered in STATE.json `platforms` — built-in AND plugin/bridge platforms. Per `../_mureo-shared/SKILL.md` → *Plugin platforms*, also enumerate any installed entry-point plugin that exposes `mcp__mureo__<plugin>_*` tools; its STATE.json platform key is `plugin:<dist>:<provider>` (the same convention mureo uses when promoting plugin mutations into `action_log`; the older `plugin:<dist>` is still read, and an entry already stored under it must be kept, not rewritten). Also enumerate any **hosted official-MCP connector** present in the session (e.g. TikTok's `tt-ads-*` tools) — its key is a first-class ad-platform key such as `tiktok_ads`; see `../_mureo-shared/SKILL.md` → *Hosted-connector platforms*.

3. **Fetch platform data**: For each registered platform:
   - **Google Ads**: prefer mureo native — call `google_ads_campaigns_list`, then `google_ads_performance_report` for the current period (last 30 days). Both work in BYOD and Live API modes. Keep the per-campaign numbers from the performance report — they become each campaign's `metrics` in step 7. If mureo's Google Ads tools are unavailable (e.g. `MUREO_DISABLE_GOOGLE_ADS=1` after `mureo providers add google-ads-official`), fall back to the official `google-ads-official` MCP's equivalent campaign-list and performance-report tools for the same period.
   - **Meta Ads**: prefer mureo native — call `meta_ads_campaigns_list`, then `meta_ads_insights_report` for the current period — capture `result_indicator` per campaign so STATE.json reflects whether each campaign's "results" are clicks or real leads, and keep the per-campaign insight numbers for the `metrics` write in step 7. If mureo's Meta Ads tools are unavailable, fall back to the official `meta-ads-official` hosted MCP for the campaign list and insights; the `result_indicator` field is mureo-specific and will not be present — record the raw optimization goal / actions list per campaign in STATE.json instead and note that CV-definition disambiguation requires mureo's native MCP. In BYOD mode this response is wrapped as `{"source": "byod_import", "as_of": <import time>, "data": [...]}` — take the rows from `data` and treat them as of `as_of`, not as live.
   - **Plugin / bridge platforms** (`plugin:<dist>:<provider>`): where the plugin exposes a daily-report / insights / performance tool (infer from the live tool list — best-effort, not deterministic), call it for the current period and keep the per-campaign numbers for the `metrics` write in step 7. Honest scope per `../_mureo-shared/SKILL.md`: capture only the basic metrics the plugin's own tools return; **skip** mureo-only value-adds (`result_indicator` CV-mismatch, anomaly detection, RSA-asset audit, rule-based scoring) — they do not exist for plugins, so omit `result_indicator` for a plugin platform. If a plugin has no such reporting tool, leave its `metrics`/`totals` empty (it stays an advisory platform) and continue — never fail the whole sync because a plugin tool is missing.
   - **Hosted-connector platforms** (`tiktok_ads` etc.): call the hosted connector's own campaign-list / reporting tools (e.g. `tt-ads-*`) for the current period and keep the per-campaign numbers for the `metrics` write in step 7. Same honest scope as a plugin platform (basic metrics only; omit `result_indicator` and other mureo-only value-adds). If the connector has no reporting tool available, leave its `metrics`/`totals` empty and continue. See `../_mureo-shared/SKILL.md` → *Hosted-connector platforms*.
   - **Ad-level delivery status — active campaigns only**: for each campaign whose status reads as active **in its own platform's vocabulary** — `ACTIVE` / `ENABLED` / `enabled`, matched **case-insensitively** (see `../_mureo-shared/SKILL.md` → *Status vocabulary contract*) — list its ads and keep each ad's `ad_id`, name, `status` **and** `effective_status`. `status` is only what the ad is configured as; `effective_status` is whether it is actually delivering, and the two disagreeing is exactly how an ad stopped outside mureo (by hand in the platform UI, by its ad set / campaign, or by policy) shows up. **Meta**: `meta_ads_ads_list` (scope by `ad_set_id`, or list once per account and group by `campaign_id`). **Google Ads**: `google_ads_ads_list` for the campaign's ad groups, keeping the ad status plus the policy review/approval status. **Amazon Ads (official-MCP bridge, `plugin:mureo-amazon-ads-bridge:amazon_ads`)**: `campaign_management-query_ad` — the body REQUIRES `accessRequestedAccount` (`{advertiserAccountId}` or `{profileId}`; a global account accepts only `profileId`) and `adProductFilter.include` with **exactly one** ad product (`SPONSORED_PRODUCTS` / `SPONSORED_BRANDS` / `SPONSORED_DISPLAY` / `SPONSORED_TELEVISION` / `AMAZON_DSP`), so a full sweep is one call per product; narrow with `campaignIdFilter.include` and `stateFilter.include` (`ENABLED` / `PAUSED` / `ARCHIVED`), and take the rows from the `ads` envelope. Amazon exposes **one state per ad** (`state`, the configured state) and no serving/review status: record it as the ad's `status` and **leave `effective_status` unset** — its effective status is **unknown**, never `state` copied across (see `../_mureo-amazon-ads/SKILL.md` → *Ad-level delivery state*). **Other plugin / hosted-connector platforms**: use the equivalent ad-listing tool where one exists; where none does, skip that platform **and report the skip** — name the platform and say that no ad-listing tool was found — so a missed or mis-inferred tool is distinguishable from a platform that genuinely has nothing to report. Cost guard: do **not** make ad-level calls for PAUSED / REMOVED campaigns. In BYOD mode these reads are served from the imported CSVs and come back wrapped as `{"source": "byod_import", "as_of": <import time>, "data": [...]}` — take the rows from `data`, and treat the statuses as of `as_of`, not as live (a live response has no such wrapper).
   - mureo BYOD data is centralized in the workspace `byod/` directory (or `~/.mureo/byod/` for legacy CLI users) and is only accessible through mureo MCP tools — do **not** look for raw CSVs in the project directory.

4. **Check data sources**: If Search Console is configured, verify site access is still valid. If GA4 is available, verify connectivity.

5. **Detect new platforms**: If new platform credentials exist but have no entry in `platforms`, prompt the user to run `/onboard` to add them.

6. **Verify STRATEGY.md Data Sources**: If STRATEGY.md is missing a `## Data Sources` section (older setup), prompt the user to add it listing all configured platforms.

7. **Update STATE.json** with all campaign snapshots. **On the Code `Write` path, use the canonical STATE.json field names** — `campaign_name` (NOT the tool-output `name`), `campaign_id` (NOT `id`), and each platform's `account_id`; a snapshot/platform missing a required field is dropped by the reporting dashboard. See `../_mureo-shared/SKILL.md` → *STATE.json Schema*. For each campaign, also write the `metrics` you fetched in step 3 onto the snapshot: `spend`, `impressions`, `clicks`, `conversions`, `cpa`, `ctr`, the Meta `result_indicator` (when present), `period` (e.g. `LAST_30_DAYS`), and `fetched_at` (ISO 8601). On Code use `Write`; on Desktop / Cowork call `mureo_state_upsert_campaign` with the `metrics` object. Record the per-platform rollup on each `platforms[<platform>]` entry too — `totals` (summed `spend` / `clicks` / `conversions` / etc.), `metrics_period` (the period the totals cover, `LAST_30_DAYS` here), and the same totals under `periods["LAST_30_DAYS"]` (the per-window map the reporting dashboard's period toggle reads) — so the dashboard can render KPIs from STATE.json without re-querying. On Code write the rollup via `Write`; on Desktop / Cowork call `mureo_state_platform_metrics_set` (pass `platform`, `account_id`, `totals`, `metrics_period="LAST_30_DAYS"`, and `periods={"LAST_30_DAYS": {…}}`) — `mureo_state_upsert_campaign` writes per-campaign `metrics` only, never the platform rollup. `periods` merges per window key, so writing `LAST_30_DAYS` never clobbers a `YESTERDAY` bucket `daily-check` wrote. On the `mureo_state_platform_metrics_set` path you may omit `fetched_at` from the rollups — the server stamps the write time onto every rollup that call supplies without one, and never re-stamps a window it only preserves; on the `Write` path set it yourself, or the dashboard has no age to show. All metric fields are optional: omit any a platform doesn't provide rather than writing nulls. Persist the ad-level statuses from step 3 too, as the campaign's `ads` array (`ad_id`, `name`, `status`, `effective_status`) — omitting `effective_status` for a platform that reports no serving status (Amazon Ads; see step 3): on Desktop / Cowork pass `ads` to `mureo_state_upsert_campaign` (the server stamps each ad's `as_of`, so do not compute it); on Code write the same array on the campaign snapshot with an `as_of` of `server_now`. Omit `ads` entirely for campaigns whose ads you did not fetch — an empty array means “fetched, this campaign has no ads”. **Plugin / bridge platforms** use the SAME shape: persist their campaigns + best-effort `metrics` (canonical vocabulary, minus `result_indicator`) under platform key `plugin:<dist>:<provider>` via `mureo_state_upsert_campaign` (pass `platform="plugin:<dist>:<provider>"` — or the key that platform is already stored under, which may be the older `plugin:<dist>`; never write a second key for a platform that already has one), plus the per-platform `totals` / `metrics_period` / `periods["LAST_30_DAYS"]` via `mureo_state_platform_metrics_set` — so a bridge's KPIs land in the same `platforms` shape the dashboard reads. A plugin with no reporting tool keeps an entry with empty `metrics`/`totals` (advisory, no synced metrics).

8. **Show diff**: Compare old vs new state and highlight changes:
   - New campaigns added
   - Campaigns removed/paused
   - Budget changes
   - Status changes
   - Bidding strategy changes, on the platforms that have one (see `../_mureo-shared/SKILL.md` → *Status vocabulary contract*)
   - **Ad-level status changes** — ads added or removed, plus **any change to a stored status field**, diffed against the `ads` the previous run stored: compare each ad's `status`, and its `effective_status` when the platform reports one, against the previous run's stored value for that *same* field, and report every difference. An ad **stopped delivering** when such a value moved away from the platform's active value — matched **case-insensitively**, so `ACTIVE`, `ENABLED` and `enabled` all count as active — which makes the limb fire on a platform that reports only one status field, in its own vocabulary, instead of falling through (see `../_mureo-shared/SKILL.md` → *Status vocabulary contract*). Changes mureo did not make are the point of this line: name them explicitly (see *Mixed operation*) instead of letting the new snapshot quietly replace the old.

9. **Update `last_synced_at`** timestamp — use `server_now` from step 0 (the `mureo_state_*` write tools stamp it for you; on the Code `Write` path write that value yourself).

If STATE.json doesn't exist yet, suggest running `/onboard` first.
