From euler
Generate a Quarterly Business Review (QBR) document for a specific partner using EULER MCP tools. Use this skill whenever the user mentions a QBR, quarterly review, partner business review, quarterly performance summary, or asks to "review", "summarize", or "report on" how a specific partner has been doing — even if they only describe the intent without using the term "QBR".
How this skill is triggered — by the user, by Claude, or both
Slash command
/euler:generate-qbrThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Invoke this skill when the user types any of:
assets/styles.cssassets/template.htmlexamples/q1-2026-axion-dataworks.mdexamples/q1-2026-bluebyte-innovations.mdexamples/q1-2026-cobalt-circuit-labs.mdexamples/q1-2026-echorise-labs.mdexamples/q1-2026-lumon-industries.htmlexamples/q1-2026-lumon-industries.mdexamples/q1-2026-primetime-video.mdreferences/mcp-field-paths.mdreferences/output-enhancements.mdInvoke this skill when the user types any of:
/euler:generate-qbrDO NOT invoke for:
performance(action: 'company') directly)Before running, confirm with the user:
partner_id.list_accounts — if the user has an account with that partner
(rare; user is usually customer-side), match affiliate_company_name
(case-insensitive).partners(action: 'list', filter_name: '<name>') — this
is the customer-admin path. Match against "Partner name" in the
response (case-insensitive substring).status field and ask the
user to pick.partners(action: 'list') without
filter, page through if needed)."Q1 2026", "Q4 2025", explicit ISO dates, "last quarter",
"this quarter".start_date / end_date in YYYY-MM-DD format:
"last quarter" or ambiguous, infer from today's date and CONFIRM with
the user before proceeding. Never silently guess.⚠️ Date format gotcha: the
performancetool's schema description says "ISO 8601 UTC datetime (e.g. '2024-04-07T00:00:00.000Z')". Empirically (validated 2026-05-25) that format is broken — the backend returns corrupted years like "Nov 28, 4763". UseYYYY-MM-DDfor ALL date params across all tools. If a future MCP release fixes the parser, revisit.
Run these EULER MCP tools in order. Default to running them all for a standard QBR. Skip a step only if the user's framing explicitly excludes it (e.g. "QBR without commissions").
| # | Tool call | Provides |
|---|---|---|
| 1 | list_accounts | Customer-side name for the QBR header (the customer hosting the partnership). |
| 2 | partners(action: 'list', filter_name: '<name>') | Resolves partner_id from a name. Skip if user provided partner_id directly. |
| 3 | performance(action: 'partner', partner_id, start_date, end_date) | Period-filtered headline metrics: deal count, booking/billings revenue, win rate, sales cycle, ACV. |
| 4 | partner_artifacts(action: 'deals', partner_id, page: 1, limit: 20) | All-time deals pipeline (NOT period-filtered — see disclaimer rule below). |
| 5 | commissions(action: 'partner', partner_id, start_date, end_date) | Period-filtered commissions paid + breakdown. |
| 6 | referrals(action: 'for_partner', partner_id, page: 1, limit: 20) | All-time referrals submitted by the partner (NOT period-filtered). |
| 7 | partner_artifacts(action: 'agreements', partner_id) | Agreement list with Status + Signed On. No expires_on field available. |
| 8 | partner_artifacts(action: 'invoices', partner_id, page: 1, limit: 20) | All-time invoices. Include only if non-empty. |
| 9 | influenced_sourced_deals(partner_id, start_date, end_date) | Deal-attribution split (Sourced vs Influenced vs Sourced-and-Influenced). Critical for tier conversations — partners want credit for the deals they touched, not just the ones they originated. |
| 10 | partners(action: 'summary') | Company-wide context for anchoring: total partner count, status distribution. Used in the TL;DR ("one of only N partners in "). |
| 11 | performance(action: 'partner', partner_id, prev_quarter_dates) | Previous quarter for Q-over-Q deltas (see "Q-over-Q rules" below). |
| 12 | performance(action: 'partner', partner_id, quarter_-2/-3/-4_dates) | Up to 4 additional historical quarters for quarter-over-quarter trend context (rendered as deltas in the hero .fact-sub / spotlight prose — the modern template has no sparklines). Skip if user explicitly asks for a fast/lite QBR. |
The MCP backend returns inconsistent field names (Bubble-side quirks).
The exact paths and serialization bugs per tool are documented in
references/mcp-field-paths.md —
consult that file when extracting fields from a specific response.
Key gotchas to keep in mind (the file has the full detail):
"Deal name", "agreement Name", ïd)referrals(for_partner) has a JSON serialization bug (commas instead
of colons in result_per_page) — parse looselylast_stage_change_date on deals is a duration string, not a
timestamp (see Rule 15)partner_not_in_consent → abort. Tell the user to disconnect + reconnect
and include this partner in their consent selection. Do not generate a partial
QBR.backend_data_issue on list_accounts → continue with partners(list)
for partner_id resolution; surface the support_email in a banner at the
top of the output.euler_session_expired / euler_user_token_missing → instruct the user
to follow Claude's inline reconnect prompt, then retry.Render a single self-contained HTML file — no external CSS, no external fonts, no script tags. A partner manager should be able to open it in a browser, paste it into email, save as PDF, or share by link, all from the same artifact.
The document is executive-readable: skimmed in 30 seconds, drilled into in 2 minutes. No internal field paths, no system terminology.
assets/styles.css and inline its full
contents into a single <style> block in <head>. Do not link to
the file — self-contained is required for portability.assets/template.html
as the skeleton. Fill in data; do not invent class names that
aren't defined in the stylesheet.<!DOCTYPE html> through
</html>. No surrounding markdown, no preamble.<html lang="pt-BR">). Localize the
five health factor labels too (Production → "Produção", etc.) but keep all five factors and the
exact weights. Proper nouns, currency, metric values, and dates stay as-is. (This applies to the
rendered report only — these SKILL instructions and CSS class names stay English.)Sections marked CONDITIONAL are omitted entirely when their underlying data is empty. Do not print "No X data" placeholders — silence the section.
Compute the partner's health per docs/partner-health-model.md
in full mode — you already fetch every source in the Orchestration sequence, so this adds no
extra calls. Use exactly these five factors and weights — do not rename, drop, re-weight, or
invent factors (no "Compliance", no 4-factor variants):
| Factor | Weight | From |
|---|---|---|
| Production | 35 | closed-won revenue + deals (window) |
| Pipeline | 20 | open pipeline value |
| Engagement | 20 | referrals submitted + recency |
| Foundation | 15 | foundational agreements signed |
| Recency | 10 | days since last deal/referral |
score = Σ(factor_norm × weight ÷ 100) → an integer 0–100. The per-factor normalization
thresholds, band cutoffs, and hard-rule caps live in the model doc — apply them verbatim.
Produce: the score 0–100, the band (Healthy / Watch / At-risk / Ramping), the factor breakdown (each factor's value out of its weight — e.g. "Production 28/35" — all five shown), and the reason (the 2–3 factors driving it + any cap that fired).
Render:
red, Watch → amber, Healthy / Ramping → default (brand). Band in the hero-eyebrow.Caps — apply only the model's exact rules; never invent one. A partner with lifetime closed-won is NOT capped merely for 0 closes in the period — its band comes from the score. The Watch cap fires only when its full condition holds (0 in-window AND 0 lifetime closed-won, or an unsigned foundational agreement AND 0 lifetime closed-won). Name a cap in the reason only then.
This replaces the old green/amber/red heuristic — score + band are the single, model-consistent signal.
Output follows the Euler design system in a modern, landing-page-style layout
(identical to portfolio-pulse and pending-approvals-triage): sticky topbar
with the Euler text wordmark → hero (eyebrow chip + headline with a gradient
.accent span + .quick-facts headline metrics) → spotlight gradient panel for
the TL;DR → numbered sections (.section-eyebrow "01 · …") with .table-wrap tables,
.quick-facts stat clusters, and .dist chips → footer.
Brand is a text wordmark, not an image. Render <span class="brand-mark">Euler</span>
in the topbar and <span class="brand-mark footer-mark">Euler</span> in the footer.
Do NOT use an <img> logo — the remote brand SVG renders broken in Claude's artifact
viewer. Keep the two <link rel="preconnect"> tags.
Lightweight & mobile-responsive (required). No JavaScript, no images, no base64
blobs, no extra web fonts beyond the two the stylesheet @imports. The sheet already
handles responsiveness (fluid clamp() type; tables scroll on narrow screens). Do not
add fixed pixel widths or inline <style> beyond inlining the provided sheet. Cap
repeated rows (top-5 open deals, ≤5 action items).
Tone classes follow the health band. .hero-eyebrow and .spotlight take
amber/red (or default brand) to match the partner's computed band (At-risk →
red, Watch → amber, Healthy / Ramping → default brand).
Follow assets/template.html. Sections, in order. Use ONLY
class names defined in the stylesheet — never improvise colors, fonts, or classes.
brand-mark "Euler" + brand-label "Q{N} {Year} Review · {Partner}".hero-eyebrow (tone) "{Q period} · {health band}" (the band from the health
model, tone-matched per above); <h1> "{Partner} — Q{N} {Year}";
subtitle = the one-line state + a .data-pill (complete/partial/stale)..quick-facts → .fact): period headline metrics — Closed-won
(Q{N}), Deals closed, Win rate, Commissions paid. Zero-denominator collapse
(Rule 4): when 0 deals closed in the period, OMIT the Win rate fact entirely
(don't render "0.00%"). Numerals render mono via .fact-value..spotlight tone) — the TL;DR + the health "why". <h2> one-line state;
<p> 2–3 sentences (biggest signal · biggest risk · the number that matters) plus the
health breakdown (the 2–3 driving factors, e.g. "Production 28/35 · Recency 3/10") and any
cap that fired, ending with <strong>Recommended next step:</strong> {one focused action}.
Wrap key figures in <span class="num">..table-wrap table — Prio (status-pill
red=P0 / amber=P1 / gray=P2) · Action (<strong> + .cell-note expected outcome)
· Owner · Due (numeric, YYYY-MM-DD). Cap 5 rows, sorted by priority..quick-facts cluster (Open deals, Closed-won
lifetime, Avg deal size — omit a Closed-lost fact if $0) + a .table-wrap of the
top 5 open deals by Amount (Deal · Stage · Amount). .note for the rollup
("+ N more open deals under $1K (…)"). Omit the whole section if no deals..dist chips): Sourced / Influenced / Sourced & influenced,
from influenced_sourced_deals. Omit if no attribution data..table-wrap — Status (status-pill 🟢/🟡/🔴) · Agreement ·
Signed (numeric date or "—"). Omit if none..quick-facts (Total submitted, In Q{N}, Most
recent) + .note (most-recent detail + test-data note if applicable). Omit if
none..note prose ("Total paid in Q{N}: $X …"). CONDITIONAL:
omit the whole section if empty..note prose ("N invoices, $X total …"). CONDITIONAL: omit
if empty.brand-mark footer-mark "Euler" + "Q{N} {Year} Business Review ·
{Partner}" + .mono "euler · generate-qbr".Model output is the complete HTML (<!DOCTYPE html> → </html>) — no surrounding
markdown, no preamble.
The pill label IS the band from docs/partner-health-model.md.
Use the band as the status-pill text; tone follows the model's mapping.
status-pill tone | Pill label | Band (from the health model) |
|---|---|---|
| (default brand) | Healthy | score ≥ 70 |
amber | Watch | 40 ≤ score < 70 (or a cap that fires Watch) |
red | At-risk | score < 40, or status = Inactive (cap → At-risk) |
| (default brand) | Ramping | status ∈ {Onboarding, Prospecting} |
Write the pill label as the band name (Healthy / Watch / At-risk / Ramping) —
never the internal tone word (amber, red). The band tells the reader what the
state MEANS.
Partner Manager — anything on the customer (your) sidePartner-side — anything the partner's team needs to doYYYY-MM-DD.Stage-age caveat (Rule against fake aging signals): the deals tool
returns last_stage_change_date as a duration string like "20599 Days"
or "389 Days" — these are NOT reliable timestamps. Do NOT write "stalled
in stage" / "no movement in N days" / "stale for X" — you do not have
aging data. Limit yourself to what stage name + Amount actually tell you:
"in Demo Scheduled at $10K" is OK; "stalled in Demo Scheduled" is not.
Status string, case-insensitive)Complete, Signed, Active, Executed, or any non-empty Signed OnPending, Draft, In Review, Out for Signature, or unsignedExpired, Terminated, Revoked, Cancelled<p class="note"> summarizing the
rollup (e.g. "+ 5 more open deals under $1K (Names...)")..fact-sub on the Closed-won fact like "7 real deals · 4 test entries excluded ($699)".The following are diagnostic-only and must never appear in the doc shown to the user:
partner_id, deal_id, agreement id, referral id) — Rule 0`performance.total_deals_count`These rules are not optional. Every QBR must follow them.
NEVER render internal EULER IDs in the output. The partner_id,
deal_id, agreement id, referral id are Bubble-internal opaque
strings — orchestration-only, never printed. When the MCP adds the
partner's CRM ID (HubSpot / Salesforce / etc), render that instead.
Empty data → silence the section. When a tool returns nothing for a section, omit the section entirely rather than printing placeholders like "No X data". The absence is itself the signal — inserting a placeholder line dilutes that signal without adding information and clutters the doc. Exception: the TL;DR may name an absence in narrative ("no closed deals this quarter") because there it is part of the story.
NEVER emit action items that ask the user to debug the system.
If performance.booking_revenue returns "$" while closed-won deals
exist in partner_artifacts(deals), normalize silently to $0 — do
NOT produce an action like "Investigate the data discrepancy." Tool
bugs are an internal problem; they belong in CONTRIBUTING / engineering
notes, never in the QBR rendered to a partner manager.
NEVER expose system internals to the reader. No source-field
annotations (no `performance.X` columns), no mentions of "the
performance tool" / "the MCP" / "Bubble" / field names, no
<!-- HTML comments --> in the rendered output. The reader is a
partner manager preparing for a partner call, not an engineer.
Zero-denominator metric collapse. If total_deals_count for the
period is 0, OMIT (do not render as zero) the rows for: win_rate,
sales_cycle, avg_contract_value. These are undefined when no
deals closed and printing "0.00%" / "0 Days" / "$0" creates false
precision.
Currency normalization. Treat "", "$", "$0", "$0.00" all as
zero. Display zero as $0. Non-zero with thousand separators
($1,234,567). Percentages as returned (75.0%).
Dates vs durations vs strings. The deals tool returns
last_stage_change_date as a duration string ("20599 Days",
"0 Days") — attempting to render it as a date produces nonsense
(we observed years like 4763 when the backend tried to parse such
inputs). Reserve YYYY-MM-DD rendering for strings that successfully
parse as a real date ("Jan 1, 2026 5:46 pm", "May 9, 2024").
When in doubt, render verbatim.
Action items must be actionable. Each row in the action table has all 5 columns filled (Prio · Action · Owner · Due · Expected outcome). No placeholder TBDs. If you cannot fill all 5 from the data + sensible defaults (see Output Format section), drop the row.
Label all-time vs period-filtered in section headings. The
period-filtered tools (performance, commissions) and the
lifetime tools (partner_artifacts, referrals) live in the same
document, and a reader has no way to tell which is which without an
explicit label. Use Q-period framing for headline sections
("What happened in Q1") and the word "lifetime" in pipeline /
referrals / agreements headings.
Q-over-Q comparison is opt-in. Default to current-quarter-only. If the user explicitly asks for QoQ, fetch both quarters' data and compute deltas. (Industry-standard QBRs include QoQ by default; we're conservative here because tool-call cost doubles.)
Loose JSON parsing required. The MCP backend returns stringified JSON-like content with known bugs:
referrals(for_partner).result_per_page: pairs use commas instead
of colons ({"id","value"} not {"id":"value"})partner_artifacts(agreements).Result[]: keys have unicode noise
(ïd with diaeresis)Parse with Number() / parseFloat / regex before computing.
If parsing fails, surface as an error — do not guess.
If a tool errors mid-run, render the rest of the doc normally and add a single line at the END (not the top) of the TL;DR:
Note:
could not be loaded due to a data fetch error. No giant red banner — the doc must still be presentable.
One partner per invocation. A QBR is partner-specific by design — narratives, action items, and the health band only make sense in context of a single partner. If the user asks for a batch ("QBR for all my partners"), explain the scope and offer to loop the skill once per partner rather than rolling up into a summary.
Tier-conditional narrative. TL;DR and action items adapt to
partner_status:
Test-data heuristic — flag, don't filter. Staging / test entries are common in this dataset. Apply these signals (case-insensitive):
asdf, test, foo, bar,
1234, purely numeric, or single-word brands matching the customer
itself (e.g. EULER / Euler when the customer is Martus)Treatment: keep them in headline counts (transparency), but call them out in the relevant section ("N entries appear to be test submissions") and add a P2 cleanup action item. Do NOT silently filter — the partner manager owns that decision.
Aging signals — bounded by data quality. last_stage_change_date
on deals comes back as a duration string. The backend has a sentinel
placeholder for "unknown" — values ≥ 9999 Days (typically "20599 Days",
which is 56+ years) are the null/garbage value and must NOT be used.
Values < 9999 Days ARE real elapsed-time signals and can be used:
Same threshold applies anywhere we see a duration string from this backend. The threshold exists because the dataset has a sentinel value (~20599 days = epoch artifact); without filtering, any aging claim would silently include garbage. With the threshold, aging is a legitimate signal.
Number formatting context. The TL;DR is a narrative; rounded
units read more naturally there ($87K, $1.2M). Tables and
detail sections sit next to figures a reader may want to sum or
spot-check, so use precise formatting with thousand separators
($87,250, $1,234,567). Rounding inside a table creates
ambiguity about whether the rounding is the underlying number or
a presentation choice.
User: "Generate a QBR for Axion DataWorks for Q1 2026"
Claude:
1. Reads this skill (generate-qbr playbook)
2. list_accounts → customer self = "Martus" (for header)
3. partners(action: 'list', filter_name: 'Axion DataWorks') →
partner_id = 1715179138375x527400652689293400
4. performance(action: 'partner', partner_id, '2026-01-01', '2026-03-31') →
0 deals, $0 booking, $0 billings (period-empty)
5. partner_artifacts(action: 'deals', partner_id) →
4 all-time deals: Target $500 Won, Best Buy $775 Demo, Honda $3000 Lost, GM $1000 Won
6. commissions(action: 'partner', ...) → empty
7. referrals(action: 'for_partner', partner_id) →
2 all-time, both May 2024
8. partner_artifacts(action: 'agreements', partner_id) →
2 pending (MNDA, Tech Partner Agreement)
9. partner_artifacts(action: 'invoices', partner_id) → empty
10. Renders markdown QBR per the template above.
User: opens the HTML in a browser → saves as PDF, or copies the rendered
page into an email / Google Doc to send to the partner. (To share in Slack,
attach the file or a link — pasting raw HTML into Slack does not render.)
references/output-enhancements.md has the
detailed rules for Q-over-Q, the Impact line, and the data-confidence indicator.
Note: that reference predates the modern template. Its old class names (
.impactspans, stat-card inline-SVG sparklines) are superseded by the component mapping below. Apply this mapping, not the old class names.
performance; render the
delta as text in the relevant hero .fact-sub (e.g. "↑ +12% vs Q3") and/or in the
spotlight prose. No separate column. Omit 0 → 0..fact cards don't host
inline charts. Use the Q-over-Q delta text instead. (Revisit only if a charting
component is added to the design system.).cell-note (in the 01 · Next quarter table)
cites concrete numbers from the data ("$50K stalled deal", "7 referrals × $15K avg
ACV"). Never invented. This replaces the old .impact span..data-pill (complete / partial / stale) in the
hero subtitle, indicating fetch completeness.A QBR currently takes a partner manager 1–2 hours per partner per quarter — opening 5 different tools, copying numbers into a deck, formatting tables by hand. With this skill the same output is one prompt + a few seconds of tool orchestration. At scale (14 partners × 4 quarters = 56 QBRs/year), the time saved is in the dozens of hours.
Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.
npx claudepluginhub euler-software-inc/euler-skills --plugin euler