Daily Performance Console
Purpose
Produce a daily operational dashboard for performance marketers running multi-platform paid media. Single-screen, blended-then-drilled, action-oriented:
- Glance KPI strip — 8 blended metrics with DoD deltas + MTD pacing flags.
- Per-channel split — health card per platform (spend, daily avg, ROAS, CPA, plan-pct meter).
- Revenue progress bar — current vs monthly plan, EOM projection, variance.
- Universal trend chart — line chart with three filter dropdowns (Granularity / Metric × 1–2 / Channels) + date-range picker.
- Tabs — Warnings (cap-exhaustion, CPA spikes, ad-disapprovals), Recommendations (3 actionable cards), Monthly Pacing (channel-stacked bar).
The console is operational (read 5–20× per day, act on warnings within hours), not a brief or a deck. Sharing happens via dashboard URL or screenshot — there is no Slack-ready text artifact.
Derived from Marketing OS use-case UC-DPA-1 v2.1 (Daily Performance Console). Canonical visual contract lives in the Improvado Obsidian vault and is mirrored as the HTML in Appendix A below — that HTML is the single source of truth for the widget structure.
This skill replaces v1.0.0 (the prior tactical Slack-brief format). The XML <analytics><changes><actions> artifact and "VP-of-Marketing voice" are deprecated. v2.0.0 ships a dashboard.
Invocation Context
Two trigger paths feed this skill:
Path A — Onboarding dispatch (new users)
Company personalization: if the dispatch message carries an alg-prebrief fenced block with a company_research: line, tailor exactly ONE headline/intro sentence in the closing chat hand-off to this company — always hedged ("looks like…"), never as a fact. If absent, proceed generically.
If the conversation contains an onboarding_summary message with Interview answers:, scan those answers for bias signals:
- Role
marketer/performance-marketer→ operator voice; KPI strip surfaces all 8 metrics; Warnings tab is the default landing tab; Recommendations card severity emphasisedcmo-director/c-level→ executive voice; subtitle prepends a one-line summary of pacing + top warning; default tab is Monthly Pacing instead of Warningsanalyst-bi→ analyst voice; Universal trend chart pre-selects two metrics (CPA + ROAS) so the dual-axis is visible from first paint; warnings + recos shown but de-emphasised
- Reconciled metrics — if
ROAS/CPAlisted as primary, pre-pin those as the default Universal-chart metrics (overrides defaultspend) - AI wish — if mentions "what's wasting money" / "wasted spend", auto-open the Action needed recommendation card; if mentions "pacing" / "vs plan", default tab → Monthly Pacing
- Reporting cadence — if mentions "daily standup" / "morning review", set auto-refresh suggestion to weekday-mornings; if "weekly review", suggest weekly cadence
Path B — Free-form chat (returning users, repeat use)
User asks naturally — "give me today's report", "what's running today", "show me the daily numbers", "where am I vs plan". No onboarding_summary; use defaults (Warnings tab open, Spend selected in chart, all channels visible).
When to Trigger
User asks for any of:
- "daily report", "daily performance", "daily KPI report"
- "daily console", "daily dashboard", "show me my numbers"
- "what's running today", "what changed this week"
- "send me the daily numbers", "leadership-ready report"
- "where am I vs plan", "pacing dashboard"
- "send to leadership", "daily VP brief"
- "tuesday vs tuesday", "drill into "
- "spend ROAS CPA today", "week over week performance"
If the user instead asks for funnel-stage / executive cross-channel narrative ("CMO dashboard", "Awareness vs Consideration vs Purchase", "where is my money going across the funnel") — fall back to a different skill (CMO Cross-Channel Dashboard, separate UC).
If the user wants an ad-hoc single-channel chart ("show me Meta spend chart") — use /business-intelligence-editor directly with the relevant single-source widget; do NOT invoke this skill.
If the user wants an exhaustive rule-quality scorecard with per-rule $-at-risk — invoke the audit-orchestrator skill instead.
Output Mode
DEFAULT → Mode B: Dashboard via /business-intelligence-editor (ONE custom-component widget)
IF user explicitly says "document" / "markdown" / "deck" → Mode A: createDocument fallback
IF user asks for one chart → Mode C: visualizationTool (single chart in chat)
Mode B: Dashboard (DEFAULT)
Use /business-intelligence-editor skill with ONE custom-component widget. The entire console (KPI strip, channel split, universal chart, tabs, warnings, recommendations, pacing) lives inside that single widget — its componentCode is the canonical HTML from Appendix A with Discovery API data substituted at the marked INJECT points.
This is the load-bearing pattern. Do NOT decompose the console into multiple BI widgets (KPI tile widget + chart widget + table widget — that breaks the visual contract). The console is ONE atomic widget.
If /business-intelligence-editor is unavailable at runtime, degrade to Mode A with a one-line note ("Saved as a document — dashboard skill not available right now.").
Mode A: Document (fallback)
Use createDocument. Markdown report with:
- KPI table (8 metrics × today / DoD / MTD / pacing flag columns)
- Channel split table
- Warnings list
- Recommendations list with $-impact
No HTML, no charts. This mode is the v1-style deliverable — kept only as a fallback when the dashboard mode is unavailable.
Mode C: Quick Visualization (single chart)
Use visualizationTool for a single chart in chat. Useful when user asks "just show me spend trend by channel". Skip the dashboard / document; return one chart with the requested metric × channel × granularity.
Auto-Refresh Strategy
The console is a point-in-time snapshot — but the natural cadence is daily. After save, offer:
"Want me to refresh this dashboard every morning at 9 AM with the latest 24h of data?"
If yes → scheduleChatTool re-runs the skill daily at 09:00 user-local. Each run pulls a fresh 30-day rolling window for the chart and refreshes today's KPI / warnings / recos.
If user mentions "weekly review" in onboarding → suggest Monday morning cadence instead.
MCP Tools Used
discoveryListConnectorsTool— find ad-platform connectors per workspacediscoveryListAccountsTool— resolve ad accounts when >1 per platform (use § 0.1 account-selection rule from Marketing OS — ask user if >5)discoveryRequestTool— fetch all KPI / channel / daily-trend / drill data (Rule 0 — see below)clickhouseTool— NOT used by default; only as fallback for 28-day σ-baseline computations if the channel's API doesn't expose historygetCurrentWorkspaceContextTool— workspace name + ID for headergetConnectionsTool— enumerate active marketing platforms (withoutdatasourceNamefilter)/business-intelligence-editorskill — Mode B dashboard savecreateDocument— Mode A fallbackvisualizationTool— Mode C single chartscheduleChatTool— daily auto-refresh
For ClickHouse-fallback table coverage (rare path, only when API history is unavailable), see discovery-api/marketing-platforms-reference.md in this repo.
Rule 0 — Discovery API mandatory for ALL data queries (HARD)
EVERY data query for this skill MUST go through discoveryRequestTool. No exceptions. No fallbacks to clickhousePalantirTool for marketing data, no direct SDK calls, no local API keys, no scraped CSVs.
This applies to:
- All per-channel KPI fetches (Meta
/insights, GooglesearchStream, TikTok/report/integrated/get/, LinkedIn/rest/adAnalytics, Pinterest/v5/.../analytics, etc.) - 28-day σ-baseline windows (use API where available; ClickHouse fallback table only if the platform doesn't expose history)
- DoD comparison data (today vs same-day-last-week)
- MTD pacing aggregates
- Channel split metrics
- Universal-chart custom date ranges
- Warning-source data (search-term reports, asset-group breakdowns, ad-disapproval lists)
- Recommendation evidence (creative CTR-decay curves, IS-lost ratios)
Flow: getAvailableClusters → createImpersonationContext → discoveryListConnectorsTool(dataSource=<channel>) → discoveryRequestTool(connectionId, method, path, body_data).
If a connector is missing — surface honestly: "No active connection for <channel> in this workspace. Connect it at to populate this section." Do NOT mock data; do NOT fall through to other sources.
Output Contract (HARD)
The result of this skill is the canonical widget HTML (Appendix A) with every INJECT point filled by Discovery API data — NOT the canonical alone, NOT the data alone, NOT regenerated HTML.
Two pieces, atomically combined:
┌────────────────────────┐ ┌──────────────────────────┐
│ Canonical widget HTML │ │ Discovery API responses │
│ (Appendix A) │ + │ (per § Discovery Mapping)│
│ — fixed structure │ │ — live workspace data │
│ — INJECT markers │ │ │
│ — visual contract │ │ │
└─────────┬──────────────┘ └────────────┬─────────────┘
│ │
└────────────► SUBSTITUTE ◄────────┘
│
▼
┌─────────────────────────────┐
│ Filled HTML → componentCode │
│ for ONE custom widget │
│ → /business-intelligence- │
│ editor save │
└─────────────────────────────┘
The deliverable is NEVER:
- The canonical alone (placeholders show literally — broken UI).
- A new HTML written from scratch (regeneration breaks visual fidelity).
- Data without the canonical (no UI to deliver).
- Canonical filled with mocked or static data (violates Rule 0).
Quick Reference
- Channels supported: Google Ads, Meta, TikTok, Pinterest, LinkedIn, Reddit, The Trade Desk (any active Discovery API ad-platform connector). The widget chip row + KPI blend adapt to whatever is connected. Minimum: 2 active channels (else fall back to single-channel ad-hoc chart).
- 8 KPIs (in this fixed order): Spend, Impressions, Clicks, CTR, Conversions, CPA, CR, ROAS.
- Universal chart: 1–2 metrics × 1–N channels × Daily/Weekly/Monthly granularity × custom date range. If 2 metrics — secondary y-axis, secondary line dashed.
- Tabs: Warnings (default) / Recommendations / Monthly Pacing.
- Recommendations: exactly 3 cards, ranked by
|$-impact|. Below threshold (<7 days data, <30 conversions, <$500 spend) — drop the candidate. - Visual chrome: product chrome (Inter typography, indigo
#4F46E5accent). NOT client-branded. No Brandfetch, no per-client palette.
STEP 0: Workspace + Connection Detection
0A. Resolve workspace context
ws = getCurrentWorkspaceContextTool(impersonation_context_id)
→ workspace_name, workspace_id
0B. Enumerate active ad-platform connections
all_conns = getConnectionsTool(impersonation_context_id) # no datasourceName filter
active = [c for c in all_conns if c.is_active and c.data_source in MARKETING_PLATFORMS]
by_ds = group_by(active, key=c.data_source)
0C. Connection count gate
IF len(by_ds) < 2:
Surface: "Daily Performance Console requires ≥2 active ad-platform connections. You have {N}.
Connect another platform, or use a single-channel chart instead."
STOP — fall back to Mode C single-channel.
0D. Account selection per channel
FOR each channel in by_ds:
accounts = discoveryListAccountsTool(connectionId=channel.id)
IF len(accounts) > 5:
Ask user which account(s) to include (don't pick blindly — § 0.1 rule).
IF len(accounts) ≥ 2 and ≤5:
Use highest-spend account by default; surface choice in subtitle.
IF len(accounts) == 1:
Auto-select.
STEP 1: Fetch Per-Channel Data (Discovery API parallel calls)
Run in parallel for every active channel. Each channel returns:
- Today's metrics (last 24h close)
- Same-day-last-week metrics (for DoD delta)
- MTD aggregates
- 30-day daily series (for chart granularity)
- Drill-deep data (search-term reports, asset-group breakdowns, ad-disapproval lists — only fetched when warnings/recos generation needs them)
Google Ads — googleAds:searchStream via discoveryRequestTool
GAQL: SELECT
segments.date,
metrics.cost_micros,
metrics.impressions,
metrics.clicks,
metrics.ctr,
metrics.average_cpc,
metrics.conversions,
metrics.conversions_value,
metrics.value_per_conversion,
metrics.search_impression_share,
metrics.search_budget_lost_impression_share
FROM customer
WHERE segments.date BETWEEN '<date_start>' AND '<date_end>'
# For warnings drill-deep: search_term_view, asset_group_performance_view, etc.
Meta — /v23.0/act_<id>/insights
fields: spend, impressions, clicks, ctr, frequency, actions, action_values, purchase_roas
level: account
time_increment: 1
date_preset: custom
since: <date_start>, until: <date_end>
# For warnings drill: level=ad_set or level=ad with breakdowns
TikTok — /v1.3/report/integrated/get/
data_level: AUCTION_ADVERTISER
dimensions: ['stat_time_day']
metrics: [spend, impressions, clicks, ctr, conversion, cost_per_conversion,
complete_payment_value, complete_payment_roas]
start_date: <date_start>
end_date: <date_end>
LinkedIn — /rest/adAnalytics?q=analytics
pivot: ACCOUNT
timeGranularity: DAILY
fields: spend, impressions, clicks, costInUsd, oneClickLeads, externalWebsiteConversions
dateRange: { start: ..., end: ... }
Pinterest — /v5/ad_accounts/<id>/analytics
granularity: DAY
columns: SPEND_IN_DOLLAR, IMPRESSION_1, CLICKTHROUGH_1, CLICKTHROUGH_RATE,
TOTAL_CONVERSIONS, COST_PER_CONVERSION, TOTAL_CONVERSION_VALUE_IN_DOLLAR
start_date: ..., end_date: ...
Per-channel timeout
8s per channel. If a channel times out → fail that channel, continue others. Surface in topbar: "Live · 12:08 UTC — Pinterest delayed (retry 12:13)". Do NOT mock; render — for missing channel cells.
STEP 2: Compute Derived Fields
After parallel fetch:
2A. Blended KPIs (top strip, 8 metrics)
today_spend = SUM channel.today.spend across channels
today_impr = SUM channel.today.impressions
today_clicks = SUM channel.today.clicks
today_ctr = today_clicks / today_impr * 100
today_conv = SUM channel.today.conversions
today_cpa = today_spend / today_conv if today_conv > 0 else null
today_cr = today_conv / today_clicks * 100 if today_clicks > 0 else null
today_revenue = SUM channel.today.conversion_value
today_roas = today_revenue / today_spend if today_spend > 0 else null
# Same for MTD aggregates (replace today.* with mtd.*)
# Same for last-week-same-day (for DoD delta)
DoD_delta(metric) = (today.metric - last_week.metric) / last_week.metric * 100
# Use 'pp' (percentage points) for CTR, CR. Use '×' for ROAS.
2B. Pacing flags per metric
pct_elapsed = days_elapsed_in_month / days_in_month * 100
mtd_pct = mtd.metric / plan.metric * 100 # uses biz_workspace_plans
deviation_pct = mtd_pct - pct_elapsed
flag = 'ok' if abs(deviation_pct) ≤ 5
else 'warn' if abs(deviation_pct) ≤ 15
else 'over'
EOM_projection = mtd.metric / pct_elapsed * 100 # linear extrapolation
2C. Channel split objects (per-channel cards)
For each channel:
mtd_spend = MTD spend
daily_avg = mtd_spend / days_elapsed
plan = biz_workspace_plans.channel_spend_plans[channel] OR Edit-plan modal
share = mtd_spend / total_mtd_spend * 100
roas, cpa = MTD-aggregated
health = 'ok' if (ROAS ≥ target AND CPA ≤ target)
else 'warn' if one missed
else 'bad' if both missed OR CPA >150% target
note = derived 1-liner from health + dominant signal
2D. Warnings — rule-based scan over fetched data
See § "Warnings rule library" below.
2E. Recommendations — rule-based candidate generator + $-impact ranking
See § "Recommendations rule library" below. Cap at 3.
2F. Pacing-block aggregation
MTD_total_spend = SUM channel.mtd_spend
Plan_total_spend = SUM channel.plan
EOM_proj_spend = MTD_total_spend / pct_elapsed
Variance = (EOM_proj_spend - Plan_total_spend) / Plan_total_spend * 100
2G. Revenue progress bar
rev_current = SUM channel.mtd.conversion_value
rev_plan = biz_workspace_plans.revenue_plan OR Edit-plan modal
rev_pct = rev_current / rev_plan * 100
rev_proj = rev_current / pct_elapsed
rev_proj_class = 'good' if rev_proj ≥ rev_plan
else 'warn' if rev_proj ≥ rev_plan * 0.95
else 'bad'
STEP 3: Discovery API → INJECT-marker mapping
This is the CONNECTING CONTRACT between Appendix A's INJECT markers and live data. For every INJECT point, the source endpoint + transformation + format.
Inline placeholders ({{INJECT:name}})
| Marker | Source | Transformation → Format | Example |
|---|---|---|---|
workspace_name |
getCurrentWorkspaceContextTool() |
.workspace_name |
Apex Outdoors |
workspace_id |
same | .workspace_id |
wkspc-7892 |
updated_at |
system clock (UTC) | HH:mm UTC |
12:08 UTC |
period_label |
derived from selected range | <MMM YYYY> of start_date |
May 2026 |
day_counter |
derived from now() + month length |
Day {dom} of {dim} · {pct}% elapsed |
Day 4 of 31 · 13% elapsed |
date_range_label |
from date-picker state | <MMM D> – <MMM D> |
Apr 4 – May 4 |
date_start, date_end |
from date-picker | ISO YYYY-MM-DD |
2026-04-04 |
rev_current |
per-channel revenue (per Step 1 + 2G) | SUM, format $N,NNN |
$58,272 |
rev_plan |
biz_workspace_plans.revenue_plan ← OR ← Edit-plan modal |
format $NNN,NNN |
$500,000 |
rev_pct |
(rev_current / rev_plan) × 100 |
1 decimal N.N% |
11.7% |
rev_proj_label |
(rev_current / pct_elapsed) + delta vs plan |
↗ Proj $XK · ±N% above|below plan |
↗ Proj $564K · +12.7% above plan |
rev_proj_class |
rule on projection vs plan | good/warn/bad |
good |
rev_fill_pct |
min((rev_current / rev_plan) × 100, 100) |
N.NN% |
11.65% |
rev_today_pct |
(today_dom / days_in_month) × 100 |
N.N% |
13.0% |
Block: channels_data (JS const C — variable channel count)
For each active channel from getConnectionsTool filtered to ad-platforms, emit one entry:
const C = {
<key>: {
name: string, // 'Google', 'Meta', 'TikTok', 'Pinterest', 'LinkedIn', 'Reddit'
color: string, // canonical brand hex (Google #4285F4, Meta #0866FF, TikTok #000000, Pinterest #E60023)
logo: string, // SVG symbol id 'logo-google' / 'logo-meta' / 'logo-tiktok' / 'logo-pinterest' (from <defs>)
mtd_spend: number, // dollars MTD
daily_avg: number, // dollars/day MTD
plan: number, // monthly spend plan from biz_workspace_plans
share: number, // % of total spend
roas: number, // ROAS x
cpa: number, // dollars
health: 'ok'|'warn'|'bad',
note: string // ≤30 chars
}
};
Block: today_kpis (JS const TODAY — 8 metrics, fixed order)
const TODAY = {
spend: { v, d, dc, mtd, plan, flag, note },
impressions: { v, d, dc, mtd, plan, flag, note },
clicks: { v, d, dc, mtd, plan, flag, note },
ctr: { v, d, dc, mtd, plan, flag, note },
conversions: { v, d, dc, mtd, plan, flag, note },
cpa: { v, d, dc, mtd, plan, flag, note },
cr: { v, d, dc, mtd, plan, flag, note },
roas: { v, d, dc, mtd, plan, flag, note }
};
// v = today's value, formatted ($N,NNN / N / N.NN% / N.NNx)
// d = DoD delta string with sign (+12% DoD, −9% DoD, +0.14pp)
// dc = 'up'|'down'|'cost-up'|'cost-down' (cost-* for spend, CPA where increase = bad)
// mtd = abbreviated MTD ($13.5K, 1.42M, 742)
// plan = abbreviated plan ($120K, 11.4M, 6,200)
// flag = 'ok'|'warn'|'over' (per Step 2B)
// note = ≤20-char pacing line ('Proj +9%', 'On track', '−9% target')
Block: daily_base (per-channel daily series)
For real data, replace the synthetic genDaily() function in canonical with pre-fetched arrays:
const DAILY = {
google: { spend: [...], impressions: [...], clicks: [...], ctr: [...],
conversions: [...], cpa: [...], cr: [...], roas: [...] },
meta: { ... },
tiktok: { ... },
pinterest: { ... }
// any other connected channel
};
Each array length = (date_end − date_start) in days. The aggregate() function in canonical handles weekly/monthly rollups from the daily base.
Block: warnings_block (rule-based scan)
| Rule | Trigger | Severity | API source |
|---|---|---|---|
| Daily-budget-cap exhaustion | projected exhaustion before EOD AND positive-ROAS campaign | critical |
per-campaign daily_budget + cumulative cost for current day |
| CPA spike vs target | CPA > target +50% AND spend ≥ $500 in last 3 days | high |
per-ad-group/ad-set CPA |
| Ad disapproval | any disapproved ad serving | info |
platform ad-status field |
| Creative fatigue | CTR decline >30% AND creative age >7 days | high |
per-creative CTR over last 14 days |
| IS lost to budget | Google search_budget_lost_impression_share >25% on positive-ROAS campaign |
high |
Google searchStream |
Cap at 6 visible warnings; surface top by severity then $-impact. Each → one <div class="warning ..."> per Appendix A widget shape.
Block: recos_block (3-card cap, $-impact ranked)
| Type | Trigger | $-impact computation |
|---|---|---|
Shift & scale (opportunity) |
Channel A: CPA >150% target AND ROAS < target; Channel B: daily-budget-capped >50% of last 30d AND ROAS > target × 1.5 | shifted_spend × (B.ROAS − A.ROAS) per week × 4 |
Pause (action) |
spend ≥ $500 over ≥5 days AND zero/near-zero conversions AND no recovery trend | weekly_spend × 4 (savings) |
Expand & test (test) |
Creative theme/audience: CTR ≥ 2× account avg AND share <30% | (target_share − current_share) × current_revenue / current_share |
Rank by |$-impact|, take top 3. Calibration threshold: require ≥7d data AND (≥30 conversions OR ≥$500 spend) on the drill entity. Below threshold → drop candidate.
Block: pacing_block (channel-stacked bar)
Channel segments come from channels_data. Header numbers from § Step 2F.
Block: plan_modal (Edit Plan defaults)
Source: biz_workspace_plans (TODO if not exists). Fields: spend_plan, conversions_plan, target_cpa, revenue_plan, target_roas, target_ctr.
STEP 4: Build Custom Widget HTML
Take the canonical HTML from Appendix A. Find every INJECT marker. Substitute per Step 3 mapping.
For each {{INJECT:name}} placeholder:
Find the literal string {{INJECT:name}} in canonical
Replace with computed value (string)
For each block marker pair:
// === INJECT: <name> ===
<existing default content>
// === END INJECT: <name> ===
Replace the inner content with the computed JSON / HTML.
KEEP the marker pair intact (so future regen can find them).
HARD RULE: if you find yourself writing <style> or restructuring <div class="kpi"> — STOP. You are regenerating, not substituting. Re-load Appendix A and substitute only at marked points. The canonical is NEVER edited per-render.
After substitution, the entire HTML is ONE string — assign it as componentCode of a single custom-component widget.
STEP 5: Save Dashboard via /business-intelligence-editor
Assemble dashboard config:
{
"dashboardTitle": "Daily Performance Console",
"dashboardSubtitle": "{N_active_channels} channels | {date_range_label} | Updated {updated_at}",
"dashboardUrl": "clients/template/dashboards/CrossChannelEditableDashboard.tsx",
"isMenuItem": true,
"defaultTimePeriod": "30",
"editState": {
"schemaVersion": 2,
"appearance": {
"hideTitle": true,
"hideFilters": true,
"colorMode": "light"
},
"widgets": [
{
"id": "main-grid-1",
"widgetType": "custom-component",
"componentCode": "<the entire substituted HTML from Step 4>"
}
],
"layout": {
"items": [
{ "i": "main-grid-1", "x": 0, "y": 0, "w": 12, "h": 24 }
]
}
}
}
appearance.hideTitle + hideFilters + colorMode: "light" matches the precedent set by weekly-creative-performance v6 — the canonical owns its own header (workspace name + chips + date picker). The dashboard chrome would duplicate / conflict.
dashboardUrl: "clients/template/dashboards/CrossChannelEditableDashboard.tsx" — emit the canonical host-TSX path directly (precedent: full-marketing-audit/dashboard-template.json:104 and weekly-creative-performance/dashboard-template.json:112 v7.4.1+). The Miras viewer (useRepoDashboardData in main/components/repo/hooks/useRepoDashboardData.ts:55) reads dashboard_url verbatim as the file path on /experimental/agent/api/repo/file?repo_id=dashboard&path=<dashboardUrl>; emitting the canonical path here removes the slug→path indirection that previously depended on BIE skill-cli's HARDCODED_DASHBOARD_URL override (skill-cli.ts:14). Earlier drafts shipped the slug "daily-performance"; that worked only when the save flowed through skill-cli — any save that bypassed it (direct curl/fetch, pre-2026-01-30 PR #415 historical save, fixture/migration) wrote the slug into the DB and the viewer 404'd with Failed to fetch repo file: 500 (Dashboard file API 404 Not Found). There is no DailyPerformanceDashboard.tsx host TSX — CrossChannelEditableDashboard.tsx renders this skill's dashboard via settings_id.
appearance and schemaVersion ride through BIE's schema via .passthrough(). editState.widgets and editState.layout.items are required by DashboardConfigSchema.
Layout: ONE widget covering full width × tall enough for the entire console (12 cols × ~24 rows in the BIE grid). The console is intentionally a single tall widget — there is NO second widget below it.
Save:
Skill('business-intelligence-editor') with the assembled JSON
# CLI form: npx tsx .claude/skills/business-intelligence-editor/skill-cli.ts save --config '<full_config_json>'
Open preview (always with --production):
python3 frontend-cli.py open-preview --production "clients/template/dashboards/CrossChannelEditableDashboard.tsx?settings_id={NEW_ID}"
Present to user with:
- Dashboard URL
- 1-line summary of what's surfaced (top warning + top reco)
- Auto-refresh suggestion (per § Auto-Refresh Strategy)
STEP 6: Auto-Refresh Suggestion
After the user sees the dashboard, offer:
"Want me to refresh this every morning at 9 AM with last-24h numbers?"
If yes → scheduleChatTool with cadence weekday-09:00-user-tz (or daily-09:00 if onboarding said "every day"). Each scheduled run re-executes the skill: re-fetches Discovery API, re-substitutes Appendix A, overwrites the saved dashboard at the same dashboardUrl. Channel logos / palette / structure stay frozen — only the data churns.
If onboarding said "weekly review" → suggest Monday-09:00 instead of daily.
Anti-patterns (HARD STOP if you find yourself doing these)
- Regenerating HTML from scratch. The canonical (Appendix A) is the source of truth. Only substitute at INJECT markers.
- Decomposing the console into multiple BIE widgets. Use ONE custom-component widget. Built-in KPI/chart/table widgets MUST NOT appear in the dashboard composition.
- Querying data outside Discovery API. Per Rule 0, every fetch goes through
discoveryRequestTool. NoclickhousePalantirToolfor marketing data. - Applying client-brand resolution. This is product chrome — fixed Inter typography + indigo
#4F46E5accent. No Brandfetch, no client logo in header, no per-client palette. - Producing the v1 Slack-text artifact. v1 deprecated. Sharing happens via dashboard URL or screenshot.
- Stopping at campaign-level for warnings/recos. Drill to keyword (search) / creative (social) / asset-group (PMax) / SKU (shopping) / line item (programmatic) / subreddit (Reddit).
- More than 3 recommendations. The 3-card cap is the value — forces $-impact selection.
- Recommendations without $-impact. Money-tied or skip.
- Hedging in recommendations. Forbidden: "may want to consider", "could potentially", "if applicable". Confident verbs only: "Pause", "Raise", "Cut", "Shift", "Expand".
- Education in the brief. "ROAS measures the return on ad spend..." — the audience knows. Skip the lecture.
- Mocked or static data in production. If Discovery API fails — surface the failure honestly with
—placeholders + retry banner. - Editing dashboard chrome.
hideTitle: true,hideFilters: true,colorMode: "light"— required for visual contract.
Tone Rules — scoped to Recommendations cards only
Apply to the 3 recommendation cards (NOT to KPI tiles, NOT to warnings — those are factual/structural):
- Confident verbs only — "Pause", "Raise", "Cut", "Shift", "Expand", "Pin". Not "consider pausing".
- No hedging. Forbidden: "may want to", "could potentially", "if applicable".
- Money-tied or skip. Each card's
.reco-impactMUST show $-value (gain or savings). If you can't quantify, drop the card. - Calibration threshold: ≥7d data AND (≥30 conversions OR ≥$500 spend) on the drill entity. Below threshold → skip the candidate.
- 3-card cap — the constraint is the value.
- No education. Audience knows the metrics.
Warnings tone: factual + actionable, brief. No tone constraints beyond clarity.
Channel-Aware Drill Matrix
When generating warnings or recommendations, drill to the lowest controllable entity per channel:
| Channel | Drill entity | Channel-native KPI | Common issue patterns |
|---|---|---|---|
| Paid Search (Google/Bing) | Keyword → ad group | Quality Score, Search IS (lost: budget vs rank), CVR/keyword | Generic broad-match leak; brand cannibalization; IS lost to budget on top converters |
| YouTube/Video (Google) | Video asset | View rate, completion (25/50/75/100%), CPV | Low completion = wrong audience or weak hook; non-skippable starving spend |
| Performance Max | Asset group → product | ROAS by asset group, conversion mix (search vs display vs YT) | Brand cannibalization; spend imbalance to display; weak audience signals |
| Shopping | Product/SKU | ROAS/SKU, IS/SKU, % zero-impression catalog | Long tail of zero-imp SKUs; OOS still serving |
| Meta | Ad creative → ad set | Hook rate (3s/imp), Hold rate (15s+/3s), Frequency, CTR | Frequency >5; single creative carrying spend; ASC w/o exclusions |
| Ad creative + audience segment | CTR vs platform avg, lead form CR, CPL by job title/seniority | Audience too narrow (<5K); single creative across segments | |
| TikTok | Ad creative | Hook rate (2s), 6s view rate, completion (p25–p100), creative freshness >7d | Stale creatives >7d; sound-off completion drop |
| Pin / creative | Save rate, outbound CTR, idea-pin completion | Static-pin fatigue; missing Verified Merchant Program tag | |
| Ad creative + subreddit | Comment sentiment, engagement rate, CVR by subreddit | Subreddit blocklist gaps; non-native creative tone |
Cross-cutting drills (all channels): pacing (delivery vs plan), dayparting/hour-of-day, device split, geo target, audience saturation / new-vs-returning mix.
Failure modes — what to render when something goes wrong
Per Rule 0 — never mock, never fall through. Behavior matrix:
| Failure mode | UI behavior |
|---|---|
| Connection missing for a channel | Drop that channel from channels_data; if active count drops <2 → fall back to Mode C single-channel chart |
| API returns NULL/empty for a metric on one channel | That channel's today_kpis contribution = 0; blended metric still renders; channel-card health flag → bad with note "No data" |
| Whole API call fails (timeout, 5xx) | Topbar banner: "Live · {time} — <channel> delayed (retry {next_retry})". Render last-known cached values if <2h old; else — placeholders WITH explicit error indicator (not the canonical's INJECT placeholders) |
biz_workspace_plans row missing |
Use last-month actuals as plan baseline; show banner "No plan set for {month} — using last-month actuals as baseline. [Set plan]" |
| Workspace has only 1 active channel | Console requires ≥2 channels. Surface message; offer Mode C single-channel chart instead. |
Quality gates
- No empty tiles in the KPI strip. If a metric is fundamentally unavailable on every active channel — render the tile but with
—and an explicit reason. - Pacing flag accuracy.
flag: 'over'means projecting >15% deviation from monthly plan. Don't markokfor missing data. - Recommendation $-impact. Every card MUST surface $-value. Never publish a recommendation without it.
- Data freshness. Live-API call timestamp shown in topbar. If stale >2h, banner: "Data delayed — last fetch HH:mm UTC".
- Visual contract. Indigo accent, Inter typography, Chart.js for trend chart, custom HTML for KPI strip and channel cards. Do not deviate.
Appendix A — Canonical Widget HTML (single source of truth)
This is the canonical visual contract for the Daily Performance Console. The widget's componentCode (per Step 5) is THIS HTML with every INJECT marker substituted by Discovery API data (per Step 3).
LLM agents performing this skill: load this section as raw text, find INJECT markers, substitute per § Discovery API mapping, output the result as the widget's componentCode string.
Mirrored at algorithms/revenue_div/marketing_dpt/01_projects/alg/claude/daily-performance/canonical.html in the team-internal Marketing OS repo for filesystem-level access during design iteration. Edit BOTH atomically when the template changes.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Daily Performance · {{INJECT:workspace_name}} — All Channels</title>
<!--
════════════════════════════════════════════════════════════════════════════
CANONICAL TEMPLATE — UC-DPA-1 Daily Performance Console
════════════════════════════════════════════════════════════════════════════
This file is the canonical visual template for UC-DPA-1.
Spec: data_sources/obsidian/vault/Improvado/05-Product/Marketing OS (Operational System)/use-cases/UC-DPA-1 Daily Performance Analysis.md
USAGE FOR LLM AGENTS:
1. Load this file as raw text
2. Find INJECT markers (see list below)
3. Substitute data between START/END markers, OR replace {{INJECT:name}} placeholders
4. Output the resulting HTML — DO NOT regenerate structure from scratch
INJECTION POINTS:
Inline placeholders ({{INJECT:name}}):
- workspace_name, workspace_id — topbar / title
- date_range_label — date picker pill text
- date_start, date_end — date picker inputs (YYYY-MM-DD)
- period_label — "May 2026"
- day_counter — "Day 4 of 31 · 13% elapsed"
- rev_current, rev_plan, rev_pct — revenue block numbers
- rev_proj_label, rev_proj_class — "↗ Proj $564K · +12.7%" / class: good|warn|bad
- rev_fill_pct, rev_today_pct — bar widths
Block markers (<!-- === INJECT: name === --> ... <!-- === END INJECT: name === -->):
- channels_data — JS const C = {...}
- today_kpis — JS const TODAY = {...}
- daily_base — JS daily-data base values per channel/metric
- warnings_block — array of warning HTML cards
- recos_block — array of recommendation HTML cards
- pacing_block — pacing tab content
- plan_modal — edit-plan modal default values
EDIT POLICY:
Update both this template AND the spec atomically.
See spec § "Data shape contracts" for required JSON shapes.
════════════════════════════════════════════════════════════════════════════
-->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
:root {
--bg: #FAFBFC;
--surface: #FFFFFF;
--surface-2: #F5F7FA;
--line: #E5E9EF;
--line-soft: #EFF2F6;
--ink: #0F172A;
--text: #1F2937;
--muted: #64748B;
--subtle: #94A3B8;
--accent: #4F46E5;
--accent-soft: #EEF2FF;
--accent-hover: #4338CA;
--good: #047857;
--good-bg: #ECFDF5;
--good-line: #A7F3D0;
--warn: #B45309;
--warn-bg: #FFFBEB;
--warn-line: #FCD34D;
--bad: #B91C1C;
--bad-bg: #FEF2F2;
--bad-line: #FCA5A5;
--info: #0369A1;
--info-bg: #F0F9FF;
--info-line: #BAE6FD;
--c-google: #4285F4;
--c-meta: #0866FF;
--c-tiktok: #000000;
--c-pinterest: #E60023;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 12px;
--shadow-sm: 0 1px 0 rgba(15,23,42,0.04);
--shadow: 0 1px 2px rgba(15,23,42,0.04), 0 1px 3px rgba(15,23,42,0.05);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font: 14px/1.5 'Inter', system-ui, -apple-system, sans-serif;
color: var(--text); background: var(--bg);
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
.num { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum'; }
.page { max-width: 1340px; margin: 0 auto; padding: 16px 24px 24px; }
/* ── LOGO MARKS ───────────────────────── */
.logo {
display: inline-block; flex: 0 0 auto;
border-radius: 4px; overflow: hidden;
width: 18px; height: 18px;
}
.logo svg { display: block; width: 100%; height: 100%; }
.logo.sm { width: 14px; height: 14px; border-radius: 3px; }
.logo.md { width: 22px; height: 22px; border-radius: 5px; }
.logo.lg { width: 28px; height: 28px; border-radius: 6px; }
/* ── TOP BAR ──────────────────────────── */
.topbar {
display: flex; align-items: center; gap: 10px;
padding-bottom: 12px; border-bottom: 1px solid var(--line);
margin-bottom: 14px;
}
.crumb { font-size: 13px; color: var(--muted); font-weight: 500; }
.crumb b { color: var(--text); font-weight: 600; }
.topbar-spacer { flex: 1; }
.control-pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 12px; border-radius: 999px;
background: var(--surface); border: 1px solid var(--line);
font-size: 12.5px; font-weight: 500; color: var(--text);
cursor: pointer; transition: all .15s;
}
.control-pill:hover { border-color: var(--subtle); background: var(--surface-2); }
.control-pill.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.control-pill.primary:hover { background: var(--accent-hover); }
.dot-live { width: 6px; height: 6px; background: #22C55E; border-radius: 50%; box-shadow: 0 0 0 3px rgba
…(truncated)