From carta-investors
Builds what-if scenario columns (trim, growth, additive) on an existing Excel budget workbook — reduce headcount, apply revenue shock, rebalance costs, model a new fund raise, or add expansion hires grounded in actuals.
How this skill is triggered — by the user, by Claude, or both
Slash command
/carta-investors:carta-budget-scenariosopusThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
[PATTERN carta-writing-style v0.0.2]
[PATTERN carta-writing-style v0.0.2] [PATTERN etiquette v0.0.6] [PATTERN text v0.0.8] [PATTERN tables v0.0.12] [PATTERN carta-watermark v0.0.10] [PATTERN base v0.1.0]
What-if modelling on top of an existing budget. Five scenario references grouped into trim and growth, plus a shared helper:
Trim:
references/headcount-reduction.md — reduce headcount by a target %.references/revenue-shock.md — apply a haircut to revenue.references/cost-rebalance.md — open-ended, user states a cash goal.Growth:
references/new-fund-raise.md — model fee revenue uplift from closing a new fund.references/expansion-hire.md — add N new FTEs at a stated comp band.Shared helper (used by Step 4):
references/get-actuals.md — canonical YTD-actuals query, so scenarios are grounded in real spend rather than just budget assumptions.Growth references can stack with each other when the user mentions multiple levers in one prompt (e.g. "raise a $500M fund AND hire 5 FTEs") — see the "Stacking" section in each growth reference.
Audience is an accountant in Excel. Plain English only. Never surface MCP
identifiers, DWH column names, UUIDs, raw JSON, SQL, or step labels.
Currency: $X,XXX positive, ($X,XXX) negative, totals bolded.
Differences are absolute. Status: ✅ Match | ⚠ Mismatch ($X diff) | ❌ Missing in Carta | ❌ Missing in Client Doc.
Closing summary link is a workbook citation (<citation:Sheet!Range>) in
Claude for Excel mode, and a file:// path in Claude Code / Cowork mode.
Every numbered choice in this skill — including the closing
next-step menu — MUST be presented via AskUserQuestion. Never
render options as a bare code-fenced markdown list. The
AskUserQuestion tool is in allowed-tools; use it. Bare-text menus
break the chooser UI in Claude for Excel and force the user to type
the number.
Trim:
Growth:
carta-create-budget.carta-budget-actuals.carta-budget-vs-actuals.carta-consolidating-pnl. A forward "how would X affect our P&L" what-if belongs here.Execute all steps silently. Do not narrate tool calls, intermediate results, or status updates. Only speak at explicit decision points: Step 0.5 (if runtime is ambiguous), Step 1 (destination chooser), Step 2 (intent routing), Step 3 (parameter gate), Step 6 (approval), and Step 8 (next-step menu).
Mandatory telemetry beacon — run this before any skip-gate check: If <SERVER> and <ENTITY_UUID> are already in context, call mcp__<SERVER>__set_context(firm_id=<ENTITY_UUID>, _instrumentation={"plugin": "carta-investors", "skills": ["carta-budget-scenarios"]}) as your very first action. Do not skip this even when all data is already available — it records this skill invocation. If either is unknown, it fires in Step 0 below.
Before Step 0, check whether these context variables are already set from an earlier budgeting skill call in the same session:
<SERVER> — connected Carta MCP server prefix<ENTITY_NAME> and <ENTITY_UUID> — the resolved entity<RUNTIME> — excel-addin or local-fileIf all four are in context: skip Steps 0 and 0.5 entirely. Call mcp__<SERVER>__set_context(firm_id=<ENTITY_UUID>, _instrumentation={"plugin": "carta-investors", "skills": ["carta-budget-scenarios"]}) to re-anchor the session scope and record this skill invocation. Proceed from Step 1 (destination chooser).
If any is missing (fresh session or cold invocation): run Steps 0 and 0.5 in order, then continue from Step 1.
Do not ask "which firm?" or "which runtime?" when those are already established from the skill the user just ran.
refresh_mcp_connectors. Filter servers[] to name matching Carta / Carta (…) / carta with status: "connected". Drop failed.mcp__claude_ai_Carta__welcome(_instrumentation={"plugin": "carta-investors", "skills": ["carta-budget-scenarios"]}), mcp__carta_production__welcome(_instrumentation={"plugin": "carta-investors", "skills": ["carta-budget-scenarios"]}), and mcp__carta__welcome(_instrumentation={"plugin": "carta-investors", "skills": ["carta-budget-scenarios"]}). First success = <SERVER>.mcp__<SERVER>__* tool before welcome — every command is gated.If none connected, list failed connectors and stop. If multiple, default to Carta (production).
Resolve firm: if user named one → mcp__<SERVER>__list_contexts(firm_name="<entity>", _instrumentation={"plugin": "carta-investors", "skills": ["carta-budget-scenarios"]}) → disambiguate via AskUserQuestion if multiple → mcp__<SERVER>__set_context(firm_id=<FIRM_UUID>, _instrumentation={"plugin": "carta-investors", "skills": ["carta-budget-scenarios"]}). Do not use call_tool for list_contexts or set_context — call the granular tools directly with _instrumentation as shown.
DWH param-name traps: dwh:execute:query takes sql: not query:. dwh:get:table_schema takes table_name: not table:. format accepts "ndjson" / "markdown", not "csv".
If no firm was named, defer to Step 3.
Set <RUNTIME> to excel-addin (open workbook) or local-file (user-supplied path). If unclear, ask via AskUserQuestion.
Branches by <RUNTIME>.
If <RUNTIME> is excel-addin:
Empty-workbook shortcut: if the active workbook has one sheet, maxRows == 0, no other tabs (typically a fresh Book1.xlsx/Sheet1), skip the chooser. Announce the rename in one sentence — "I'll use the empty workbook you have open and rename Sheet1 to Scenarios." — then proceed. The chooser only exists to protect non-empty state.
Where should the scenarios live?
Scenarios tab" (recommended for >3 scenarios or wide pivots).If <RUNTIME> is local-file:
Where should the scenarios live?
Scenarios sheet to the same file" (recommended).<budget>-scenarios.xlsx file alongside the original" (preserves the original).Store <DESTINATION> (open workbook + tab in add-in mode, or .xlsx
path + sheet name in local-file mode).
| Phrase | Reference |
|---|---|
| "cut headcount", "reduce salaries", "team reduction", "trim staffing", "headcount X%" | headcount-reduction.md |
| "revenue shortfall", "revenue haircut", "if revenue drops", "demand shock" | revenue-shock.md |
| "preserve $X cash", "hit a cash target", "free up cash", "propose ways to reduce spend" | cost-rebalance.md |
| "raise a new fund", "Fund closing", "new fund raise", "AUM uplift", "management fee impact of a new fund" | new-fund-raise.md |
| "hire N FTEs", "add headcount", "expand the team", "hire ramp", "model new hires" | expansion-hire.md |
Multi-lever prompts. If the user names more than one of the above in a
single prompt (e.g. "raise a $500M fund AND hire 5 FTEs"), route to every
matching reference. Combine their outputs into composed scenarios — each
scenario column reflects the joint effect of all selected levers, and the
cash-impact summary breaks out each leg (e.g. New-Fund Fees, New Personnel,
Net). Do NOT run the references serially as separate outputs.
Immediately call read_skill for every matched reference — do not reconstruct scenario logic from memory:
| Reference matched | Call |
|---|---|
| headcount-reduction | read_skill(file_path="references/headcount-reduction.md") |
| revenue-shock | read_skill(file_path="references/revenue-shock.md") |
| cost-rebalance | read_skill(file_path="references/cost-rebalance.md") |
| new-fund-raise | read_skill(file_path="references/new-fund-raise.md") |
| expansion-hire | read_skill(file_path="references/expansion-hire.md") |
Reference-specific. Generally include:
If <RUNTIME> is excel-addin: use the add-in's read tools.
If <RUNTIME> is local-file:
uv run "${CLAUDE_PLUGIN_ROOT}/scripts/read_workbook.py" \
"<BUDGET_PATH>" --sheet "<BUDGET_SHEET>"
Pull YTD actuals via
references/get-actuals.md
so scenarios are grounded in real spend rather than just budget
assumptions.
Each reference computes its own scenarios. Outputs are always relative to the base budget as live formulas where possible — so the user can edit inputs and see scenarios recalculate.
=Base!H12 * 0.9 for a 10% trim.=fund_size * fee_rate * months_after_close / 12. Never hardcode the
fund size, fee rate, hire count, or comp band — those must be editable
cells so the user can flex them post-build.Preview table:
| Section | Line Item | Base | Scenario 1 | Scenario 2 | Scenario 3 | Recommended Δ |
|---|---|---|---|---|---|---|
| Personnel | Salaries | $4,200,000 | $3,780,000 | $3,360,000 | $2,940,000 | ($420,000) |
(Sample row shows the format — currency with $, negatives in parentheses, totals would be bolded. Column header is Recommended Δ regardless of trim vs growth — the value is the absolute change for the recommended scenario.)
Plus a cash-impact summary block at the bottom of the preview. Columns depend on the reference(s) that ran:
Scenario | Annual Spend Δ | Projected Cash at Year-End | NOI ΔNew-Fund Fees Y1, New Personnel Y1, etc.).Net column showing the combined effect.Output the preview table and cash-impact summary above as a normal conversation message. Then call AskUserQuestion immediately after — the question field must be a single short sentence; never include preview content inside it.
question: "Approve writing these scenarios?"header: "Approval"multiSelect: falseoptions:
description: "Writes the scenario columns to the destination chosen in Step 1.")The ← recommended marker goes inside the description field of option 1, not as a suffix on the label.
Wait for explicit OK.
Hard rule: no workbook-write tool (Excel-add-in cell write, execute_office_js that mutates state, write_workbook.py, or any equivalent) runs before this gate's AskUserQuestion returns the user's explicit "Approve and write" choice. If you catch yourself about to call a workbook-write tool without that approval recorded, stop and run this gate first.
Before calling execute_office_js with state-mutating code, setValues, write_workbook.py, or any other workbook-write tool, look back at your tool history. Find the most recent AskUserQuestion you sent. Does its answer literally include "Approve and write the scenarios"? If NO, Step 6 did not pass — send the Step 6 approval menu now and wait for the explicit answer.
Do not interpret upstream answers as approval. A parameter response, a routing answer, or any prior AskUserQuestion whose answer is not literally "Approve and write the scenarios" does NOT clear this gate.
execute_office_js calls (excel-addin runtime)The most common failure mode is bundling cell writes + formatting + logo into one writeSheet(...) function — the model writes the cells, returns, and forgets the logo. Do not combine the cell-write call with the brand block in a single office.js block.
execute_office_js. Return.const keys = blobs.keys();
return { blobKeys: keys };
If blobKeys is empty ([]), stop immediately — do NOT attempt the brand block and do NOT proceed to the summary. Surface this message to the user:
"The Carta logo asset isn't loaded yet in this session. Please close the skill panel, wait a moment, then reopen it and try again. The scenario data has been written — only the branding step is pending." Do not silently skip branding. Do not mark the branding step as complete. Do not write Step 8 summary text.
branding-and-header.md. Only run this after Call 1.5 confirms blobKeys is non-empty.CartaLogo exists.Returning from Call 1 does NOT finish Step 7. The verification call must appear in your tool history before Step 8 summary.
Before any write, call both of these in the same message (parallel reads):
read_skill(file_path="references/branding-and-header.md") — 4-row metadata band, logo placement, blobs.getText asset pattern, cell-comment API.read_skill(file_path="references/<scenario-reference-from-step-2>.md") — the scenario file(s) matched in Step 2 (call one per reference for multi-lever prompts).Do not reconstruct either spec from memory. All files must be in your context before generating any execute_office_js or write_workbook.py code. The branding-and-header.md file defines the reserved 4-row metadata band (A1 firm / A2 descriptive title like "2026 Budget · Senior-heavy trim scenario" / A3 source / A4 other context), the Carta logo placement (column E, rows 1–3 height), the blobs.getText("assets/...") asset-loading pattern for Excel add-in (NOT Read), and the cell-comment pattern for any low-confidence flag. If the existing Budget tab does not already have the 4-row band, add it as part of this write (shift data via sheet.getRange("1:5").insert(...)).
If <RUNTIME> is excel-addin: use the add-in's cell-write tools.
Either side-by-side columns next to the existing budget, or a new
Scenarios tab with a header section explaining each scenario's
assumptions. After cell writes, brand every tab the skill touched —
both the existing Budget tab (if a header band was inserted) and any
new Scenarios tab — using the verbatim brand block in
branding-and-header.md.
If <RUNTIME> is local-file:
uv run "${CLAUDE_PLUGIN_ROOT}/scripts/write_workbook.py" --stdin <<'JSON'
{
"workbook_path": "<DESTINATION>",
"operations": [ ... ]
}
JSON
Mark one scenario ← recommended in the cash-impact summary based on
whichever best meets the user's goal (least painful trim that hits the
target, smoothest NOI impact, etc.).
After running the brand block for every tab this skill touched, run this verification as a separate execute_office_js call before proceeding to Step 8:
const tabs = [/* "Scenarios", "Budget 2026", ... — substitute the actual tab names touched */];
const result = {};
for (const tabName of tabs) {
const sheet = context.workbook.worksheets.getItem(tabName);
sheet.shapes.load("items/name");
await context.sync();
result[tabName] = sheet.shapes.items.map(s => s.name);
}
return result;
The result must show CartaLogo in every tab's shape list. If any tab lacks CartaLogo, re-run the brand block for that tab and re-verify. Do not start Step 8 summary text until this verification returns CartaLogo on every tab.
Never silently skip branding. If the verification returns [] or an empty shape list, that means the brand block did not run successfully — do NOT proceed. If blobs were empty (Call 1.5 returned []), surface the reload message above and stop. If blobs were available but addImage still failed, surface the error and let the user decide.
Step 8 precondition (DO NOT SKIP). Before sending the summary text below, scan your tool history. Three anchors MUST be present in that order:
AskUserQuestion whose answer included "Approve and write the scenarios" — Step 6 approval.sheet.shapes.addImage(base64) call for each tab the skill touched — Step 7 branding.execute_office_js whose result showed CartaLogo on every tab — Step 7 verification.If any anchor is missing, STOP, go back, and run the missing gate. Do NOT write "Carta logo placed at..." in the summary when no shapes.addImage call appears in your tool history.
Open the summary with a verb that matches the reference(s) that ran:
new-fund-raise → "Modeled the fund-raise impact at 3 close sizes…"expansion-hire → "Modeled 3 hire-ramp scenarios…"If <RUNTIME> is excel-addin:
Modeled 3 trim options for Example MgmtCo. Scenario 2 (Senior-heavy reduction) preserves $487,000 of cash at year-end with the smallest impact on Q1 momentum — recommended. Full breakdown on Scenarios.
If <RUNTIME> is local-file:
Modeled 3 trim options for Example MgmtCo. Scenario 2 (Senior-heavy reduction) preserves $487,000 of cash at year-end — recommended. Full breakdown written to
Scenariosinfile:///path/to/<budget-workbook>.xlsx.
The next-step menu MUST be a single AskUserQuestion call with the options below as options entries. Never render them as a numbered markdown list, a bulleted list, or inline prose — bare-text menus break the chooser UI in Claude for Excel and force the user to type the number. The ← recommended marker goes inside the description field of one option, not as a suffix on the label.
Call AskUserQuestion with these exact parameters:
question: "What would you like to do next?"header: "Next step"multiSelect: falseoptions: the four label + description pairs above (place ← recommended in the description field of the recommended option, NOT in the label)DO NOT render the menu as inline markdown text, a numbered list, a bulleted list, or closing prose. If your response is about to contain 1. ..., 2. ..., 3. ..., 4. ... as a list at the end of the summary instead of an AskUserQuestion tool call, you have failed this gate — back up and invoke the tool.
Mark option 1 ← recommended after the first scenario set; option 3
if the user is exploring "what would the new pacing look like".
When the user selects an option, immediately invoke the corresponding skill via Skill('<skill-name>') BEFORE doing any work. Do not freelance the output — load the downstream skill's SKILL.md so its gates, layout spec, branding rules, and approval flow apply. Routing:
| Option | Skill to invoke |
|---|---|
| 1 — Model a different scenario type | Skill('carta-investors:carta-budget-scenarios') re-entry with the new scenario type |
| 2 — Drill into one of the scenarios | Stay in this skill — render the impacted-lines breakdown inline |
| 3 — Run a fresh pacing analysis using the scenario as baseline | Skill('carta-investors:carta-budget-vs-actuals') |
| 4 — I'm done | No invocation; close cleanly |
Queries > 50 rows: request format: "ndjson", bucket into a blob. Don't paste large results — triggers context_snip. Use "markdown" only for ≤50-row previews.
='Budget 2026'!H12 * 0.9 syntax (single quotes around sheet names with spaces).Across-the-board / Junior-heavy / $300M close / Base + Fund V. Never Bull / Base / Bear, Optimistic / Pessimistic, Best case / Worst case — describe what changed, not how to feel.range.merge(true) discards trailing cells. Insert a new row first.' or use numberFormat: "mmm yyyy" on a real date.+, =, -, or @ parses as a formula. Prefix such text with a leading ' (or a space), or drop the leading symbol — including Δ-delta and + Δ scenario column headers.Worksheet.position in a separate context.sync(), never in the same statement as worksheets.add(). Create and activate first: const sh = sheets.add(name); sh.activate(); await context.sync();.welcome, or the currency on the base budget data); if it can't be resolved, ask the user. Use the locale-specific format token — never a bare $ (renders as system symbol on non-US locales): USD [$$-en-US]#,##0.00_);([$$-en-US]#,##0.00);"-", EUR [$€-x-euro2]#,##0.00_);([$€-x-euro2]#,##0.00);"-", GBP [$£-en-GB]#,##0.00_);([$£-en-GB]#,##0.00);"-", CAD [$CA$-en-CA]#,##0.00_);([$CA$-en-CA]#,##0.00);"-". The A4 band reads Amounts in <resolved_currency>.style = "Continuous", then weight = "Thin". Never style: "Thin".execute_office_js block (Call 1), in this order — never a separate call: restore automatic calc → context.workbook.application.calculate(Excel.CalculationType.full) → set fixed widths on label columns → autofit numeric columns only → context.sync(). Pattern:
context.workbook.application.calculationMode = Excel.CalculationMode.automatic;
context.workbook.application.calculate(Excel.CalculationType.full);
sheet.getRange("A:A").format.columnWidth = 120; // Section — fixed
sheet.getRange("B:B").format.columnWidth = 180; // Line Item — fixed
sheet.getRange("C:<last_col>").format.autofitColumns(); // Base + scenario columns only
await context.sync();
Why fixed widths for label columns: autofitColumns() on the full range sizes column A to the widest section name, making it excessively wide. Always set A and B explicitly, autofit only C onwards. Recalc before autofit: scenario values are live formulas — without the forced recalc they stay at 0 and accounting format shows - (then #### after autofit). In local-file mode, add a set_column_width op for columns A and B, then autofit_columns over C onwards. Never autofit a header-only range.references/branding-and-header.md for every tab. Rows 1–4 reserved, logo at column E, blobs.getText("assets/...") for asset access.Never auto-retry. Always surface the failure and let the user decide.
Mgmt Fee — <Fund>.headcount-reduction.md GL patterns, confirm placement.$300M close, $500M close). Never carry sentiment into the workbook.McpAuthError) → prefix mismatch, NOT auth. Re-run refresh_mcp_connectors, probe matching prefix's welcome. Never tell user to re-auth without verifying.npx claudepluginhub anthropics/claude-plugins-official --plugin carta-investorsBuilds or restructures a fund/ManCo budget workbook in Excel from Carta prior-year actuals. Groups line items into sections with subtotals and applies inflation/contingency buffers.
Builds 3-5 year financial models for startups with cohort revenue projections, cost structure, headcount planning, cash flow, key metrics, and three-scenario analysis.
Reviews startup financial models for investor readiness—validates unit economics, stress-tests runway scenarios, benchmarks metrics against stage targets. Supports Excel, CSV, Google Sheets exports.