agentsclimarketplace

Daily check

Skill logly/mureo/skills/daily-check

Run a daily health check on all configured ad accounts (Google Ads, Meta Ads, Amazon Ads, TikTok Ads, Search Console, GA4, and any configured plugin platform). Use when the user asks for a daily review, health check, status update, anomaly detection, or 'how are my campaigns doing today'. Reads STRATEGY.md and STATE.json, runs platform-specific health diagnostics, checks goal progress, evaluates pending action_log observations, and reports findings as Healthy / Watch / Action-needed. Also use when the user asks in Japanese (デイリーチェック / 今日のアカウント状況は / 異常がないか確認して / 日次ヘルスチェック).From its SKILL.md

Install
npx -y skills add logly/mureo --skill daily-check

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

SKILL.md

45.5 KB, as published. Nobody here has run it

Daily Check

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).

Run a daily health check on all marketing accounts using the strategy context.

Modes

Pick the mode before step 0 — it decides how much you gather and how you report:

  • Incremental (default) — the cheap daily run. Gather compact findings analytics-first (step 4), load the action_log as a subset (step 0), and report only what changed since the previous reports.daily summary (step 10's diff-first contract). This is what keeps context small and the report short.
  • Deep — the full run: every step below at full depth (raw per-platform pulls, the full action_log, the full Healthy / Watch / Action-needed report). Use it when the user says "deep" or "full", on the first-ever run, or when the previous reports.daily summary is missing or older than 7 days (compare its generated_at / period against server_now's date). Recommend running a deep check once a week so the incremental diffs never drift from ground truth.

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.

Step 2b imports what mureo can fetch, so much of that work is now in action_log as origin: "external" before you start diagnosing. Two rules follow. First, an entry with origin: "external" is something mureo OBSERVED — never describe it as something mureo did, and never offer to roll it back (mureo refuses; it never saw the prior value). Second, and more important, import covers only the platforms with a change feed: for every platform step 2b reports unavailable or error, you are still working blind, and a quiet report from such a platform is not evidence that it was quiet. Say which platforms those were.

Prerequisites

  • STRATEGY.md and STATE.json must exist in the current directory (run the onboard skill first if not)

Steps

Before you start: Run the Diagnostic preamble from ../_mureo-shared/SKILL.md — load learning insights (mureo_learning_insights_get) and consult advisors (mureo_consult_advisor) before drawing conclusions.

  1. 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 — an ISO 8601 timestamp with UTC offset, e.g. 2026-07-28T10:12:33+09:00. Its date is the only source of the current date for this run; do not ask the user, do not guess, and do not shell out (this skill must run in Bash-less headless hosts). Every other date you will see — reports.daily.period, last_synced_at, action_log timestamps — is history, never evidence of what day it is now. Also never write server_now into STATE.json: it is a response field, and a persisted copy becomes tomorrow's stale "today".

    Scope the action_log to save context. In incremental mode call mureo_state_get with action_log: "pending" — it returns only the entries with an OPEN observation_due (the past-due ones you owe an evaluation this run in step 9, and the future-due ones still under observation), and adds action_log_scope: "pending" + action_log_total: <int> so you know the log you were shown is a subset, not the whole history (never read it as complete — see Mixed operation). In deep mode omit the parameter (or pass action_log: "all") to load the full log.

    Never short-circuit on history. Do not conclude "today's report is already fetched — re-displaying it" (or skip a step) because a stored period / last_synced_at looks like today. A daily check always fetches fresh platform data for the period ending on server_now's date; a stored report is at best a prior run to compare against.

  2. Load context: Read STRATEGY.md (especially Operation Mode, Data Sources, and all Goal sections) and STATE.json (the same mureo_state_get response from step 0 on MCP hosts). In incremental mode that response's action_log is the pending subset (marked by action_log_scope) — all the evidence loop in step 9 needs; deep mode carries the full log.

  3. Discover available platforms: Identify all configured platforms from STATE.json platforms and check which data sources (Search Console, GA4) are accessible. Also enumerate installed plugin / bridged platforms (mcp__mureo__<plugin>_* tools, plus Amazon Ads under Amazon's own tool names — STATE.json key plugin:mureo-amazon-ads-bridge:amazon_ads) and any hosted official-MCP connector present in the session (e.g. TikTok's tt-ads-* tools, STATE.json key tiktok_ads); include them best-effort — see _mureo-shared → Plugin platforms and Hosted-connector platforms. For every platform you enumerate, record whether its tools are callable in this session, not just whether its key is configured: a hosted connector is enumerated from STATE.json platforms alone, so one that is disconnected host-side stays in scope with its whole tool namespace missing. That is the input step 4's absent-tool-surface branch reads.

2b. Import changes made outside mureo: call mureo_external_changes_import (no arguments — it covers every platform in STATE.json). It appends anything mureo did not do to the action_log with origin: "external", skipping changes already imported and changes mureo itself made. Do this before step 3's sync, so a status you are about to diff already has its explanation attached. Safe to re-run; importing the same change twice is a no-op.

Read the response for the blind spots, not just the finds. blind_spots lists every platform that came back unavailable (not checked — either mureo has no change feed for it, or a feed could not answer for this account, e.g. BYOD mode, which has no change history; notes says which — see docs/change-import.md) or error (the feed exists but could not be read). Neither means "no changes". Carry those platform names into step 10's report and state that manual work there is invisible to mureo; when truncated_platforms is non-empty, add that older changes inside that window are unreachable and cannot be recovered later. If the tool is unavailable (older mureo), say so and fall back to the step-11 status diff alone — and note that the diff sees only status changes, not budget, targeting or creative edits.

Newly imported entries carry an observation_due anchored on when the change actually happened, so an older one is already past due and comes back to you in step 9. See ../_mureo-shared/SKILL.md → Changes made outside mureo.

  1. Sync state: For each platform in STATE.json platforms, fetch current campaign data and update STATE.json.

    • Google Ads: prefer mureo native google_ads_campaigns_list. If mureo's Google Ads tools are unavailable (i.e. MUREO_DISABLE_GOOGLE_ADS=1 was set when the user installed the official MCP via mureo providers add google-ads-official), fall back to the official google-ads-official MCP's equivalent campaign-list tool (typically list_campaigns or report_campaigns).
    • Meta Ads: prefer mureo native meta_ads_campaigns_list. If unavailable, fall back to the official meta-ads-official hosted MCP's campaign-list tool (the official MCP exposes the Marketing API surface, so basic listing is available). 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.
    • TikTok Ads (hosted connector, tiktok_ads): mureo has no native TikTok tools — call the TikTok connector's own campaign-list / reporting tools (e.g. tt-ads-*) for the current period, and write the per-campaign numbers to STATE.json via mureo_state_upsert_campaign (platform="tiktok_ads"). Skip mureo-only fields (result_indicator, anomaly, RSA audit). See _mureo-shared → 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). This ad-level fetch runs in both modes — it is cheap and the external-change diff in step 11 needs it.
    • mureo BYOD mode applies only to mureo native tools — do not look for raw CSVs in the project directory; 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.
  2. Platform health checks: Run health diagnostics on each configured ad platform.

    • Auth failure is not data — it is a hole in the report. Any tool result carrying "status": "auth_error" means mureo could not read that platform in steps 3–4 at all: auth_cause is no_credentials (nothing is configured) or token_invalid (a credential exists and the platform rejected it — typically an expired token), and detail is the operator-facing sentence. Never render detail where a metric belongs, and never read that platform's missing or empty numbers as "quiet" — unreadable and quiet are different facts. Record the platform name with its auth_cause and carry both into step 10 exactly as step 2b's blind_spots are carried, then keep going for every other platform: one platform's credentials failing degrades the report, it never aborts the run.
    • An absent tool surface is the same hole, and nothing will report it to you. A platform is also unreadable when there is no tool to call: its key is in STATE.json platforms but its tools are not in this session at all — a hosted connector disconnected host-side, so the entire tt-ads-* namespace is missing. There is no call to make and no error envelope to catch, so unlike auth_error this one has to be checked before you report, not after a result comes back: for each platform from step 2, confirm you actually called a tool of that platform's own and got rows back. Exhaust the fallbacks first — mureo native, then that platform's official / hosted MCP, per step 3's sync bullets and the per-platform bullets below — and raise tools_absent_for_<platform> only when no surface remained to try, so this never fires on a platform a fallback could still read. Then treat it exactly as auth_error: record the platform, carry it to step 10, and keep going for every other platform.
    • Analytics-first (both modes' first move). Consult mureo_analytics_modules_list for every platform (not just external ones) — it maps each platform to its advertised capabilities (detect_anomalies, diagnose_performance, audit_creative, analyze_budget_efficiency, detect_delivery_collapse). Match its platform field against the platform keys you already hold: it is the canonical platform key (plugin:<dist>:<provider> for a plugin, and the older plugin:<dist> still resolves), the same key STATE.json and action_log use, while registry_name / source_distribution are identifiers, not keys — never look a platform up by them (see ../_mureo-shared/SKILL.md → Canonical platform key). Then per platform:
      • Module registered AND advertises the capability you need → run it via mureo_analytics_run (DETECT_ANOMALIES / DIAGNOSE_PERFORMANCE; pass platform, capability, account_id; add window_days for detect_anomalies or scope for diagnose_performance) and fold its compact findings (result) into the health summary. In incremental mode this REPLACES the raw pulls — do not ALSO fetch raw campaign / insights rows to re-derive what the module already summarized. A status other than ok (no_analytics_module / capability_not_available / error) means treat that capability as unavailable and fall through to the raw checks below — never fail the whole daily-check because one platform's module errored.
      • No analytics module, or the module lacks the capability → report analytics_not_available_for_<platform> for the module-specific deep diagnostics (RSA audit, result_indicator, budget-efficiency) — <platform> being the canonical platform key, so the notice names the same platform as the rest of the report — and do NOT fabricate heuristics from the integration's tool schemas (Issue #120). BUT still run the generic, platform-agnostic analysis_anomalies_check per campaign (metrics normalized to campaign_id/cost/clicks/impressions/conversions/cpa/ctr): it builds a median baseline from action_log and flags zero-spend / CPA-spike / CTR-drop for TikTok, plugins and official-MCP platforms too — a principled detector, not an auto-derived heuristic (see _mureo-shared → analytics-module parity → generic anomaly check).
      • Deep mode runs the raw per-platform diagnostics below in full regardless of what analytics advertise; incremental mode runs them only as the fallback above (a platform with no analytics module, or a module that lacks the capability).
    • Delivery collapse — run this in BOTH modes, for every platform. A campaign that is still ENABLED while its impressions have gone to zero is the single most detectable failure mode in ad ops, and it is the inverse of the cost-spike checks above: google_ads_cost_increase_investigate finds spend that ran away, this finds delivery that stopped. Detection is cheap and unattended — never skip it to save context.
      • Platform has an analytics module advertising detect_delivery_collapse (Google Ads and Meta Ads do) → mureo_analytics_run with capability="detect_delivery_collapse". Do not pass window_days: the baseline is a same-weekday median and needs weeks of daily data, so the 7-day default would make it unusable. Read the result's status before its signals: only status="ok" makes an empty signals list an all-clear. status="no_credentials" and status="data_unavailable" mean nothing was checked — report them as such, never as "no problems found".
      • Then read unreported_days before concluding anything. It is the number of complete days the platform has reported no delivery for, on any campaign. 0 is the healthy case. A non-zero value is not a collapse — a platform that lags by a day is routine, and mureo deliberately does not guess — but an empty signals list next to a non-zero unreported_days is not an all-clear either: those days were never evaluated. If it is 2 or more, or has grown since yesterday, report it as Action needed: "the platform has reported no delivery since reported_through" — a total account outage and a platform-side reporting failure look identical from here, and only the operator can tell them apart in the UI.
      • Blind spot to state, not to hide: a campaign with no rows at all in the window is invisible to this detector (no first row, no series, and mureo will not invent one). If a campaign you expect to see is absent from evaluated_campaigns, say so rather than treating it as healthy.
      • Every other platform (TikTok and other hosted connectors, Amazon via the official-MCP bridge, plugin platforms) → pull a day-grain report from that platform's own tools covering the last ~35 days, normalise each row to {campaign_id, campaign_name, status, end_date, date, impressions, clicks, cost}, and pass them to analysis_delivery_collapse_check with the canonical platform key. It returns the same reported_through / unreported_days fields — read them the same way. Pass rows for all campaigns in one call, not one campaign at a time: a campaign that is still delivering is what proves the platform covered those dates, and without it a silent campaign cannot be distinguished from an unreported one. And pass rows from a single fetch — never stitch together rows you retrieved at different times. The check treats the newest date anywhere in rows as how far the platform has reported, so mixing a freshly-pulled campaign with a staler one makes the fresh one's frontier the yardstick and reports the staler (healthy) campaign as collapsed. If you cannot avoid mixing — or the connector finalises campaigns at different times — set reported_through to the oldest per-campaign last date you trust; it costs a day or two of recency and never hides a real collapse. Never pass the end of the range you asked for. The detector is the same code the native platforms run. If a platform cannot produce day-grain delivery at all, say so — delivery_collapse_not_checkable_for_<platform> — instead of leaving the impression it was checked.
      • Do not hand-roll the thresholds. The detector already suppresses the false positives that would get it muted: weekend/weekday seasonality (same-weekday baseline), the partial current day, intentional pauses, finished flights, low-volume campaigns, and campaigns with under two weeks of days that actually delivered. It is also independent of how long the outage has been running and of whether the platform emits zero-delivery rows at all. The operator tunes it in STRATEGY.md ## Guardrails (delivery_collapse_drop_pct, delivery_collapse_consecutive_days, delivery_collapse_min_baseline_impressions, delivery_collapse_baseline_days); read them, do not re-derive them.
      • A signal is always Action needed, and it goes in the report in both modes (see step 10's safety rule — an unresolved outage never goes quiet). Report campaign_name, collapse_start_date, days_at_collapse and the baseline it fell from. To find out WHY, run /rescue → Delivery collapse — do not guess a cause here.
    • Google Ads (built-in module — capabilities detect_anomalies, diagnose_performance; raw pulls run in deep mode, or as the incremental fallback per Analytics-first above): prefer mureo native — google_ads_performance_report (campaign-level metrics — works in BYOD), google_ads_health_check_all (returns [] in BYOD; only meaningful with Live API), google_ads_cost_increase_investigate (per-campaign anomaly check), google_ads_monitoring_zero_conversions (per-campaign). If mureo's Google Ads tools are unavailable, fall back to the official google-ads-official MCP for the basic performance report; skip the mureo-only anomaly-detection tools (google_ads_health_check_all, google_ads_cost_increase_investigate, google_ads_monitoring_zero_conversions) and note to the user that those checks require mureo's native MCP (install or re-enable via mureo setup claude-code).
    • Meta Ads (built-in module — capabilities detect_anomalies, diagnose_performance; raw pulls run in deep mode, or as the incremental fallback per Analytics-first above): prefer mureo native meta_ads_insights_report — surfaces a result_indicator field per campaign (actions:link_click vs actions:offsite_conversion.fb_pixel_lead); use this to detect CV-definition mismatches across campaigns where one campaign's "results" are clicks while another's are real leads. If mureo's Meta Ads tools are unavailable, fall back to the official meta-ads-official hosted MCP for the basic insights / campaign listing surface; note that the result_indicator analysis is a mureo-specific value-add and will not be present in the official MCP's responses.
    • In mureo BYOD some tools return [] for unsupported features (auction insights, ad performance) — that's by design, not missing data; carry on with the rest of the diagnostics.
    • Present a unified health summary across all platforms, including any analytics_not_available_for_<platform> notices.
  3. Mode-specific checks based on Operation Mode:

    • ONBOARDING_LEARNING: This mode means the campaign is in its learning period — NOT merely that mureo was recently set up. First confirm it still applies: check each platform's learning status and the accumulated conversion history. If the campaigns are actually mature (well past the learning window, with accumulated conversions — common when mureo is added to an already long-running account), do not blindly warn "prioritize data accumulation". Instead proceed with the full analysis and proactively offer to switch the Operation Mode to a steady-state mode (e.g. EFFICIENCY_STABILIZE). Only hold back on changes when the data confirms a genuine learning period is still underway.
    • EFFICIENCY_STABILIZE: Analyze CPA trends across all platforms. Flag if CPA increased >10% on any platform.
    • TURNAROUND_RESCUE: Identify zero-conversion campaigns and cost spikes across all platforms.
    • SCALE_EXPANSION: Check budget utilization across all platforms. Flag underspending campaigns.
    • COMPETITOR_DEFENSE: Run each platform's competitive insights where it publishes them — on Google Ads, auction insights and impression share — and flag impression share drops >5%. A platform that publishes no competitive data contributes nothing to this mode: report that gap rather than inferring competitor activity from its own delivery numbers.
    • CREATIVE_TESTING: Audit ad asset performance across all platforms. Flag underperforming creatives.
    • LTV_QUALITY_FOCUS: Review search term quality and audience alignment across all platforms.

    Learning-state guard (all modes): An entity inside a platform learning state — a Meta ad set in its learning phase, a Google Ads bid strategy showing "Learning" after a strategy or target change — is Watch at most, never Action needed on efficiency grounds. Its cost per result is not steady-state, so do not read it as performance and do not propose edits that would reset the clock. See ../_mureo-pro-diagnosis/SKILL.md → Allocation & Learning-State Discipline.

    Delivery-state guard (all modes): An entity that was not delivering for part of the period — paused, disapproved, or budget-exhausted, read off the delivery status you fetched in step 3 (effective_status when the platform reports one, otherwise status) — is Watch at most, never Action needed on efficiency grounds. Its cost per result is not steady-state either: the spend and results cover only the hours it actually ran, so the report must state that its figures are partial rather than reading them as performance. And never recommend pausing an entity that is already not delivering — the pause has happened, what it needs is an explanation, not the same action again. When an entity carries no effective_status at all, its delivery state is unknown — not delivering. A stored status with no effective_status beside it is only the configured state, so it tells you the entity is not paused; it is never evidence that it served. Report such an entity's delivery state as unknown, take delivery evidence from spend / impressions in the period instead, and say so when you have none. Unknown does not by itself trip this guard — it removes the right to assert steady-state delivery, not the numbers. (Today's example: Amazon Ads, whose ads expose a single state — ENABLED / PAUSED / ARCHIVED — and therefore store no effective_status.)

  4. Organic search pulse (if Search Console is available): Check top organic queries for the site. Identify any organic ranking drops that may need paid coverage, or organic gains where paid spend can be reduced.

  5. On-site behavior check (if GA4 is available): Correlate ad platform metrics with on-site behavior — LP conversion rates, bounce rates, session quality. Flag discrepancies between ad platform and GA4 conversion data.

  6. Goal progress check: For each Goal in STRATEGY.md, gather current metric values from all relevant platforms and data sources (ad platforms, GA4, Search Console) based on each Goal's declared platform/source. Present a Goal progress summary:

    Goal: CPA < 5,000 -- Platform A: 4,800 OK, Platform B: 6,200 OVER TARGET
    Goal: CV >= 100/month -- Platform A: 62, Platform B: 28, Total: 90 AT RISK
    Goal: Organic clicks +20% -- Search Console: +12% IN PROGRESS
    
  7. Evidence check: Review action_log entries that have observation_due dates (in incremental mode these are exactly the pending subset step 0 returned) — compare each one against server_now's date, never against another action_log timestamp:

    • For entries whose observation window has passed: collect current metrics for the same campaign and call mureo_outcome_evaluate with before = the entry's metrics_at_action and after = the current metrics. It returns a deterministic per-metric + overall improved / regressed / inconclusive verdict (applying the ±noise band and metric directions for you) — use it instead of eyeballing the numbers, then report with the confidence it implies (see _mureo-learning skill). This tool is pure/platform-agnostic, so it works for any platform including hosted connectors and plugins — for those, first normalize the connector's metric names to the standard keys (cpa, conversions, ctr, cost, …) so they can be scored (unknown names are treated as neutral/no-verdict).
      • Then close the observation. mureo_outcome_evaluate is pure — it records nothing — so you MUST append an evaluation record via mureo_state_action_log_append: action = a one-line summary of the verdict, evaluation_of = that source entry's index. Use the index field carried on the pending entry (deep mode: its position in the full log). This takes the entry out of the pending set so it is not re-evaluated tomorrow (without it, the observation stays open forever and the pending set grows without bound).
    • For entries still within their observation window: note them as "pending observation" and do NOT recommend further changes to the same target on the same platform. The action log records one canonical target: use entity_type + entity_id when present (an explicitly declared generic target), otherwise ad_id, otherwise campaign_id. Parent ids required only as API context (for example Google's ad_group_id beside an ad_id) are not targets. Only suppress the whole campaign when campaign is the finest identity recorded; a pending ad-group / ad-set / placement action must not block unrelated entities in that campaign. If no identity is present, report the pending observation but do not pretend it matches a current recommendation.
    • Entries already closed by a later evaluation_of (or rollback_of) are done — do not re-evaluate them. In incremental mode you will not even see them: the pending scope from step 0 already excludes closed entries.
    • platform="plugin:<dist>:<provider>" entries participate in this loop on equal footing with built-ins; they have no metrics_at_action baseline, so evaluate them qualitatively/advisory (see _mureo-shared → Mutating plugin tools — structural strategy parity).
    • Entries with origin: "external" (imported at step 2b) participate on equal footing too, with two differences. They have no metrics_at_action — mureo was not present when the change was made — so evaluate them qualitatively, never by scoring a delta against a baseline that does not exist. And their outcome must be phrased as what happened after a change someone else made: if it needs undoing, that is a manual action in the platform, because rollback_plan_get refuses every external entry. Their occurred_at is when the change happened; timestamp is only when mureo noticed it, so measure the window from the former.
    • Do NOT attribute metric movements to specific actions without checking sample sizes and observation windows.
  8. Report — the mode decides the shape:

    Partial report (both modes) — this goes FIRST, above the verdicts. If any platform came back "status": "auth_error" in steps 3–4, or was recorded tools_absent_for_<platform> there, this report is partial: open it by saying so and name each affected platform with why it was not checked and the recovery — auth_error carries its auth_cause (no_credentials → configure the credential via mureo configure / mureo auth setup; token_invalid → re-authorize, the credential is present and dead), and tools_absent means the platform's tools were not in this session, so reconnect the connector host-side (or drop the platform from STATE.json if it is gone for good). Then withhold every verdict, goal-progress line and recommendation that depends on that platform's numbers — an unreadable platform has no performance to judge, and cross-platform advice (budget shifts, "scale here, cut there") derived only from the platforms that did answer is not the advice you would give with all of them. Report the platforms that did answer as usual, and make clear which is which. Never estimate or extrapolate what you could not read. A platform that was not checked gets no KPI table, no target/actual columns, no attainment percentage and no pass/fail icon — those say "measured against a target", which is exactly what did not happen, and a table shaped like the measured ones is what the reader acts on however honest the prose above it was. A one-line disclaimer does not license the table underneath it: name the platform, say it was not checked, and stop there.

    Deep mode — full report. Summarize findings as: Healthy (no action needed), Watch (minor issues to monitor), Action needed (requires immediate attention). For each issue, suggest specific actions aligned with the current Operation Mode. Do NOT recommend actions based on single-day fluctuations — at least 7 consecutive days of critical metrics (>30% off target) before suggesting rescue.

    Incremental mode — diff-first. Compare this run against the previous reports.daily summary and report ONLY the deltas:

    • Verdict transitions — any campaign/platform that moved between Healthy / Watch / Action needed since yesterday.
    • New and resolved findings — flags raised this run, and flags present yesterday but gone today (name what resolved, e.g. "CPA back under target on Brand").
    • External changes — what step 2b imported (origin: "external" entries, with what changed and when), plus the campaign- / ad-level status diffs from step 11's machinery. Also report step 2b's blind_spots: the platforms mureo could not check at all. A platform that was not checked is not a platform that was quiet.
    • Evidence-check verdicts that came due — the step-9 outcomes whose observation window closed this run.
    • Goal progress — only when a threshold was crossed or the trend flipped; otherwise omit it. Everything unchanged collapses to ONE line, e.g. N campaigns unchanged since yesterday (Healthy). Safety rule: every current Action needed item is reported in full even when unchanged from yesterday — an unresolved problem must never go quiet. If the previous reports.daily summary is missing (first run, or it was never persisted), say so and fall back to the deep full report — there is no anchor to diff against.

    Output discipline (both modes): do not reproduce raw tool output tables in the report — give verdict lines and deltas only, and cite raw numbers only where they justify a flag (a threshold crossed, an anomaly). The reader wants the verdict, not the spreadsheet.

  9. Update STATE.json: Update campaign snapshots, add notes for flagged issues, and log this daily check to the action_log with a summary of findings. Persist the ad-level statuses from step 3 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), on Code write the same array with an as_of of server_now; omit ads for campaigns whose ads you did not fetch. Then report Ad-level status changes against the ads the previous run stored — ads added or removed, plus any change to a stored status field: 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). Report Campaign-level status changes the same way: each campaign's status against the status the previous run stored on its CampaignSnapshot, same case-insensitive comparison. A campaign- or ad-level change with no matching action_log entry was made outside mureo: surface it in step 10's findings (see Mixed operation) rather than absorbing it silently, and treat its spend as part of the performance you are explaining. Check step 2b's import first — an entry with origin: "external" on that same target IS the matching record, and reporting the diff a second time as unexplained would double-count one change. On the Code Write path, use the canonical STATE.json field names — campaign_name (NOT the tool-output name), campaign_id, and each platform's account_id; a snapshot/platform missing a required field is dropped by the reporting dashboard. On the Code Write path also stamp the top-level last_synced_at to server_now so the dashboard's "Synced N ago" freshness reflects this run (the mureo_state_upsert_campaign / _platform_metrics_set tools set it for you). See ../_mureo-shared/SKILL.md → STATE.json Schema.

  10. Persist the report summary (best-effort): Call mureo_state_report_set with report="daily" and a concise summary object so the read-only dashboard can render this report without re-running you. Persist in incremental mode too — step 10's diff-first report is computed against this stored summary, so skipping it blinds tomorrow's run. Follow this convention:

    • generated_at: ISO 8601 timestamp of this run — use server_now
    • period: the day reviewed, derived from server_now (e.g. "2026-06-17")
    • totals: the account's headline figures, using the canonical metric vocabulary — spend, conversions, cpa, ctr, clicks, impressions. This is the block the dashboard renders as figures. Raw numbers only: 773957, not "¥773,957"; 0.0466, not "4.66%" — one of those keys carrying a string is refused, because it sits where the view reads a figure and renders as nothing. A key outside the vocabulary may ride along: it is stored, just not shown as a headline number. Omit totals entirely if this run gathered no account-level figures.
    • kpis: the per-platform split — the breakdown, not the headline row — measured only. Omit a platform that was not checked (auth_error / tools_absent) rather than persisting an estimate for it, and record the gap as a flag ({code:"custom", severity:"action", label:"<platform> not checked: <cause>"}) rather than as a sentence in narrative: tomorrow's incremental run diffs against this object, so a figure invented here becomes the baseline it trusts.
    • flags: a list of structured flags — each a small object {code, severity, params} so the dashboard renders a coarse, localizable chip with the numbers on drill-down:
      • code: a canonical vocabulary key — one of goals_met, cpa_over_target, cpa_under_target, cv_below_target, cv_above_target, spend_spike, cpa_spike, invalid_traffic_suspected, zero_cv_adspots, budget_overspend, budget_drift, tracking_suspect, zero_conversions, supply_tools_unconfigured, anomaly_baseline_insufficient, pending_observations, search_console_no_property, ga4_not_configured.
      • severity: one of action/watch/info/positive (omit to take the code's default — info/positive keep informational and good-news flags visually distinct from alarms).
      • params: an object holding the DETAIL (adspot ids, yen, ctr, dates). Keep detail in params, NOT baked into the code — write {"code":"invalid_traffic_suspected","params":{"adspot":"4311492","spend":115740,"cv":0,"ctr":0.0466}}, never a slug like adspot_4311492_invalid_traffic_spike_115740yen.
      • For a finding outside the vocabulary use {code:"custom", severity, label} where label is a string or a {"ja":…,"en":…} map. Unknown non-custom codes are rejected. (A legacy bare-string flag still works but renders without the drill-down — prefer the object form.)
      • Example: [{"code":"invalid_traffic_suspected","params":{"adspot":"4311492","spend":115740,"cv":0,"ctr":0.0466}}, {"code":"budget_drift","params":{"live":170000,"configured":150000,"unlogged":true}}, {"code":"goals_met"}]
    • narrative: the Healthy / Watch / Action-needed verdict and what you propose next, at most 400 characters — the tool refuses a longer one rather than truncating it (a sentence cut in half is worse than a long one). Do not restate the figures and do not list the findings here: numbers belong in totals, findings in flags — see ../_mureo-strategy/SKILL.md → Reports section for the before/after of the paragraph this replaces.

    Reflect the FINAL state, and persist this LAST. Write this summary AFTER every STATE change and action_log entry you made this run (it is the last persistence step — keep it after any Operation Mode switch, campaign pause, budget edit, etc.). If you changed something, the narrative + flags must describe the POST-change state (e.g. "switched to EFFICIENCY_STABILIZE", not "recommend switching"; flag {code:"custom", severity:"info", label:"Switched to EFFICIENCY_STABILIZE"}, not a now-resolved recommendation). Stamp generated_at at the moment you write this — so the dashboard's "Latest report" is never an older snapshot that contradicts the action log or STRATEGY.md. (If you only recommended a change and did NOT make it, the recommendation wording is correct — and there is then no matching action_log entry, so nothing contradicts it.)

    This is best-effort: if mureo_state_report_set is unavailable (e.g. a pure file-mode host without the context MCP), skip it silently — the rest of this skill still works.

  11. Persist per-window per-platform rollups (best-effort): so the reporting dashboard's KPI tiles AND its Yesterday / Last 30 days period toggle have data, write each platform's totals to platforms[<platform>].periods[<window>] using the canonical metric vocabulary (spend, impressions, clicks, conversions, cpa, ctr, the Meta result_indicator when present). Write every window whose numbers you already gathered this run — typically YESTERDAY (the prior-day totals) and LAST_30_DAYS (the trailing-30-day totals your step-4 health checks / step-8 goal progress already pulled). The period toggle only appears once two windows have data, so populating LAST_30_DAYS here — from numbers you already hold — is what makes the toggle show even when a separate sync-state run has not happened. On Code use Write; on Desktop / Cowork call mureo_state_platform_metrics_set (pass platform, account_id, and periods={"YESTERDAY": {…}, "LAST_30_DAYS": {…}} — include only the windows you actually have) — it merges per window key, so this never disturbs a window a prior sync-state wrote. Honest scope / cost: only write a window whose numbers you actually pulled; do not fire an extra API call solely to populate a window, and write nothing at all for a platform recorded auth_error or tools_absent_for_<platform> this run — it has no numbers, and an empty tile is honest where an extrapolated one is not. In incremental mode you may only hold the window your analytics run covered — write just that one, and never mislabel a wider window as YESTERDAY. Best-effort: if mureo_state_platform_metrics_set is unavailable, skip silently. For a platform you could NOT collect, record why — call mureo_state_platform_not_collected_set (platform, account_id, reason: the auth_error / tools_absent_for_<platform> finding in one actionable sentence). Writing no numbers is right, but silence alone leaves the card indistinguishable from an account that genuinely spent nothing, which is how a stale card goes unnoticed for days. Symmetrically, for every platform you DID collect, clear any note by calling the same tool with reason omitted — nothing else retires it, and a note that outlives its failure is worse than none.

  12. Persist the day-grain history (best-effort, zero extra platform API calls): step 4's delivery-collapse check already pulled a day-grain delivery report for the platforms it checked that way, and those rows are discarded the moment the check returns — so platforms[<platform>].periods can say what yesterday was and nothing can say whether it beat the day before. Fold the rows you already hold into platforms[<platform>].daily, a map keyed by calendar date (YYYY-MM-DD) whose values are totals-shaped rollups in the same canonical metric vocabulary (spend, impressions, clicks, conversions, cpa, ctr): sum that platform's per-campaign rows per date, one bucket per day. On Desktop / Cowork call mureo_state_platform_daily_set (pass platform, account_id, and days={"2026-08-20": {…}, "2026-08-19": {…}}); on Code use Write. It merges per date key, so a day already stored survives this write and re-writing a day just replaces that day. Never fire a platform call to fill this in — this step writes what step 4 fetched and nothing else; a platform whose collapse check ran server-side (mureo_analytics_run) returns findings, not rows, so you hold no daily rows for it and write none. Write only complete PAST days — today is still being spent into, and a part-spent day stored as a whole one is a false low that nothing ever corrects; the tool refuses today and any later date. Omit a day you have no figures for — never write zeros for it: a zero-filled day is indistinguishable from an account that stopped spending, and both the trend line and the collapse detector's baseline read it as one. Nothing needs backfilling: the history accumulates one run at a time, and mureo keeps the most recent 35 days. Best-effort: if mureo_state_platform_daily_set is unavailable, skip silently.

Keep looking

Skills are one crate of 628,853. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.