sync-state
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 wh
SKILL.mdskills/sync-state/SKILL.md
GitHubSync State
PREREQUISITE: Read
../_mureo-shared/SKILL.mdfor auth, security rules, output format, and Tool Selection (Read/Write on Code,mureo_strategy_*/mureo_state_*MCP on Desktop / Cowork), and../_mureo-state-schema/SKILL.mdfor the STATE.json schema and the status vocabulary contract.
Tool names below refer to the built-in platforms; a namespaced variant (see
_mureo-shared→ Namespaced built-in variants) is used with its prefix substituted.
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
onboardskill first if not)
Steps
Note: the Diagnostic preamble (learning insights + advisor consult) from
../_mureo-shared/SKILL.mdis intentionally not required here — sync-state is mechanical data synchronization with no judgement calls.
Establish today: call
mureo_state_getfirst, on every host (including Claude Code, where you would otherwiseReadthe file) and takeserver_nowfrom 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_logtimestamps — are history: use them to describe what changed, never to decide what "now" is or whether a sync is still needed. Never writeserver_nowinto STATE.json; it is a response field, and a persisted copy becomes tomorrow's stale "today".Read current STATE.json (if exists) to track changes — the same
mureo_state_getresponse from step 0 on MCP hosts.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 exposesmcp__mureo__<plugin>_*tools; its STATE.json platform key isplugin:<dist>:<provider>(the same convention mureo uses when promoting plugin mutations intoaction_log; the olderplugin:<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'stt-ads-*tools) — its key is a first-class ad-platform key such astiktok_ads; see../_mureo-shared/SKILL.md→ Hosted-connector platforms.Fetch platform data: For each registered platform:
- Google Ads: prefer mureo native — call
google_ads_campaigns_list, thengoogle_ads_performance_reportfor 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'smetricsin step 7. If mureo's Google Ads tools are unavailable (e.g.MUREO_DISABLE_GOOGLE_ADS=1aftermureo providers add google-ads-official), fall back to the officialgoogle-ads-officialMCP's equivalent campaign-list and performance-report tools for the same period. - Meta Ads: prefer mureo native — call
meta_ads_campaigns_list, thenmeta_ads_insights_reportfor the current period — captureresult_indicatorper campaign so STATE.json reflects whether each campaign's "results" are clicks or real leads, and keep the per-campaign insight numbers for themetricswrite in step 7. If mureo's Meta Ads tools are unavailable, fall back to the officialmeta-ads-officialhosted MCP for the campaign list and insights; theresult_indicatorfield 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 fromdataand treat them as ofas_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 themetricswrite 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_indicatorCV-mismatch, anomaly detection, RSA-asset audit, rule-based scoring) — they do not exist for plugins, so omitresult_indicatorfor a plugin platform. If a plugin has no such reporting tool, leave itsmetrics/totalsempty (it stays an advisory platform) and continue — never fail the whole sync because a plugin tool is missing. - Hosted-connector platforms (
tiktok_adsetc.): 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 themetricswrite in step 7. Same honest scope as a plugin platform (basic metrics only; omitresult_indicatorand other mureo-only value-adds). If the connector has no reporting tool available, leave itsmetrics/totalsempty 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-state-schema/SKILL.md→ Status vocabulary contract) — list its ads and keep each ad'sad_id, name,statusandeffective_status.statusis only what the ad is configured as;effective_statusis 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 byad_set_id, or list once per account and group bycampaign_id). Google Ads:google_ads_ads_listfor 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 REQUIRESaccessRequestedAccount({advertiserAccountId}or{profileId}; a global account accepts onlyprofileId) andadProductFilter.includewith exactly one ad product (SPONSORED_PRODUCTS/SPONSORED_BRANDS/SPONSORED_DISPLAY/SPONSORED_TELEVISION/AMAZON_DSP), so a full sweep is one call per product; narrow withcampaignIdFilter.includeandstateFilter.include(ENABLED/PAUSED/ARCHIVED), and take the rows from theadsenvelope. Amazon exposes one state per ad (state, the configured state) and no serving/review status: record it as the ad'sstatusand leaveeffective_statusunset — its effective status is unknown, neverstatecopied 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 fromdata, and treat the statuses as ofas_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.
- Google Ads: prefer mureo native — call
Check data sources: If Search Console is configured, verify site access is still valid. If GA4 is available, verify connectivity.
Detect new platforms: If new platform credentials exist but have no entry in
platforms, prompt the user to run/onboardto add them.Verify STRATEGY.md Data Sources: If STRATEGY.md is missing a
## Data Sourcessection (older setup), prompt the user to add it listing all configured platforms.Update STATE.json with all campaign snapshots. On the Code
Writepath, use the canonical STATE.json field names —campaign_name(NOT the tool-outputname),campaign_id(NOTid), and each platform'saccount_id; a snapshot/platform missing a required field is dropped by the reporting dashboard. See../_mureo-state-schema/SKILL.md→ STATE.json Schema. For each campaign, also write themetricsyou fetched in step 3 onto the snapshot:spend,impressions,clicks,conversions,cpa,ctr, the Metaresult_indicator(when present),period(e.g.LAST_30_DAYS), andfetched_at(ISO 8601). On Code useWrite; on Desktop / Cowork callmureo_state_upsert_campaignwith themetricsobject. Record the per-platform rollup on eachplatforms[<platform>]entry too —totals(summedspend/clicks/conversions/ etc.),metrics_period(the period the totals cover,LAST_30_DAYShere), and the same totals underperiods["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 viaWrite; on Desktop / Cowork callmureo_state_platform_metrics_set(passplatform,account_id,totals,metrics_period="LAST_30_DAYS", andperiods={"LAST_30_DAYS": {…}}) —mureo_state_upsert_campaignwrites per-campaignmetricsonly, never the platform rollup.periodsmerges per window key, so writingLAST_30_DAYSnever clobbers aYESTERDAYbucketdaily-checkwrote. On themureo_state_platform_metrics_setpath you may omitfetched_atfrom 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 theWritepath set it yourself, or the dashboard has no age to show. Give theLAST_30_DAYSrollup aperiod_end— the last calendar date those figures cover,YYYY-MM-DD, in the ad account's own timezone (the server stampsfetched_atbut does not know the account's timezone and will not guess this date; without it the card is judged on when it was last written, not on what it covers). 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'sadsarray (ad_id,name,status,effective_status) — omittingeffective_statusfor a platform that reports no serving status (Amazon Ads; see step 3): on Desktop / Cowork passadstomureo_state_upsert_campaign(the server stamps each ad'sas_of, so do not compute it); on Code write the same array on the campaign snapshot with anas_ofofserver_now. Omitadsentirely 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-effortmetrics(canonical vocabulary, minusresult_indicator) under platform keyplugin:<dist>:<provider>viamureo_state_upsert_campaign(passplatform="plugin:<dist>:<provider>"— or the key that platform is already stored under, which may be the olderplugin:<dist>; never write a second key for a platform that already has one), plus the per-platformtotals/metrics_period/periods["LAST_30_DAYS"]viamureo_state_platform_metrics_set— so a bridge's KPIs land in the sameplatformsshape the dashboard reads. A plugin with no reporting tool keeps an entry with emptymetrics/totals(advisory, no synced metrics).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-state-schema/SKILL.md→ Status vocabulary contract) - Ad-level status changes — ads added or removed, plus any change to a stored status field, diffed against the
adsthe previous run stored: compare each ad'sstatus, and itseffective_statuswhen 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, soACTIVE,ENABLEDandenabledall 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-state-schema/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.
Update
last_synced_attimestamp — useserver_nowfrom step 0 (themureo_state_*write tools stamp it for you; on the CodeWritepath write that value yourself).
If STATE.json doesn't exist yet, suggest running /onboard first.