<carta-plugin>carta-investors:6.38.0</carta-plugin>
<!-- Part of the official Carta AI Agent Plugin -->Explore Data
Query the Carta data warehouse for investors data — NAV, performance metrics, cash flow statements, balance sheets, portfolio financials, and more.
Schema-first rule for raw SQL: Whenever you fall through to
dwh__execute__querydirectly — bypassingexecute:questionand the semantic-layer steps — you MUST calldwh__list__tables(omitschemato enumerate all schemas) and thendwh__get__table_schemato confirm the exact table path and every column name before composing the query. Never infer or guess column names from context or semantics; the live schema is the only source of truth. One preflight eliminates the two leading error classes:invalid identifier(wrong column name) andObject does not exist(wrong table name) on the first attempt.
When to Use
This is the skill for Carta Web / Fund Admin data work — the data warehouse. Note that Carta Fund Forecasting (formerly Tactyc) is a separate domain with its own funds and data; when a fund performance question could belong to either system, the fund-performance.md semantic layer will automatically check Fund Forecasting first before running DWH queries.
- Always use when the user context is set to a
Firmand the request involves any Carta Web / Fund Admin data query, financial metric, or reporting question - Do NOT use for funds that live in Carta Fund Forecasting (formerly Tactyc) — that is a separate domain with its own data; use
carta-fund-forecastingfor performance metrics (TVPI/DPI/IRR/MOIC/NAV/reserves) of those funds. When the fund system is unknown for a performance query,fund-performance.mdprobes Fund Forecasting automatically and redirects if the fund is found there - Always use for portfolio queries, holdings questions, fund breakdowns, or "what is [firm/fund] invested in" phrasing — even though those phrases appear in
carta-soi's trigger list;carta-soiis for building persistent Cowork artifacts, not answering data questions inline - Always use for read-only valuation data (409a history, FMV, MOIC, investment metrics) — even though "valuations" and "portfolio companies" appear in
carta-portfolio-valuations; that skill is for running and updating valuation projects, not reading data - Also use when no context is set and the user asks an ambiguous investment or data question — this skill will guide them through context setup via
list_contexts/set_context
Prerequisites
The user must have the Carta MCP server connected. If this is the first query in the session:
- Call
list_contextsto see which firms are accessible - Call
set_contextwith the targetfirm_idif needed - For cap table queries — confirm the corporation ID before running. If the user names a portfolio company, resolve its
CORPORATION_IDfromCORPORATION_BASIC_INFO_V2first (see Step 2 table below)
Named-entity queries do NOT need
list_contexts.list_contextsonly enumerates the Carta client firms the user has admin access to — never call it to search for a person, LP, or portfolio-company name. Once firm context is already set and the user names an entity that isn't the active firm (e.g. "Armstrong Capital Partners' capital activity inception to date"), that name is almost always an LP/investor within the current firm, not a request to switch firms — proceed straight to Step 1 (dwh__execute__question) with the question scoped to that name. Only calllist_contextsagain if the user explicitly asks to switch to a different firm.
Tool priority (firm context):
fa:*MCP commands →dwh__execute__question→ semantic-layer SQL (Steps 2–4) → rawdwh__execute__query. Never callcap_table:*orcap_table_chartin firm context — those require a direct tenant role unavailable to investor-portal portcos; use the DWH queries incap-table.mdinstead.
Step 0 — Fetch portfolio companies (MANDATORY GATE)
After setting context, always fetch the list of portfolio companies the user has access to:
Required even for specific-company queries — establishes accessible companies and resolves corporation_id values needed for cap table queries.
- If the result is empty, tell the user their firm context may not be set correctly and call
list_contextsto diagnose. - If the user asked about a specific company, use the result to resolve the exact
corporation_idfor that company before continuing to Step 1.
Step 1 — Try execute:question (PRIMARY query path)
Structural questions ("what tables exist?", "what columns does X have?") skip Steps 1–3 entirely. Go directly to
dwh__list__tables(omitschemato list all) ordwh__get__table_schema. Do not runexecute:questionfor schema discovery — it has no visibility into raw table structure and will hallucinate.
Before loading any semantic layer, call the plain-English query interface with the user's question verbatim (or lightly rephrased for clarity):
If the call succeeds and returns meaningful rows → format and present the results using the General Presentation Rules below. Stop here — do not continue to Steps 2–4.
Fall through to Step 2 when any of the following occur:
- The tool returns an error or exception
- The result set is empty and the user's question implies data should exist
- The returned columns don't match what the user asked for (e.g. wrong metric, wrong granularity)
- The tool indicates it cannot interpret the question or lacks the required data
Do NOT retry execute:question with a rephrased question — fall through immediately.
Step 2 — Identify the Query Domain
Use this table to pick the right context file before running any query:
LOAN_OPS firm-scoping rule (MANDATORY — cross-tenant exposure risk): The active firm context does NOT scope
LOAN_OPStables.set_contextdoes not filter them, and a fund-admin firm scope drops everyLOAN_OPStable to 0 rows — the row-access policy does not resolve it (seecarta-loan-dashboard). An unfiltered query therefore silently returns rows that belong to other firms, with no error or warning. Every query againstLOAN_OPS.LOAN,LOAN_OPS.LENDER_POSITION, or any otherLOAN_OPStable MUST include an explicit firm filter:WHERE LENDING_FIRM_NAME = '<firm name>'(orWHERE LENDING_FIRM_ID = <id>). Before you present the results, confirm that every returned row belongs to the expected firm — discard and re-query if any row does not. Note:dwh__list__tablesdoes not enumerateLOAN_OPS; probe a table by name withSELECT * FROM LOAN_OPS.<TABLE> LIMIT 1instead.
Step 3 — Load the Context File
Read the matching file from ${CLAUDE_PLUGIN_ROOT}/skills/carta-explore-data/semantic-layer/<domain>.md:
The file contains the SQL query, column reference, and presentation rules for that domain. Follow them exactly.
Cap table prerequisite check — before loading
cap-table.md, verify:
- The MCP context is set to a firm (not a fund or LP). Call
list_contextsif unsure.- A
CORPORATION_UUIDis available. If the user named a company, resolve it fromCORPORATION_BASIC_INFO_V2— match by name, UUID, or integer ID depending on what the user supplied: If multiple matches are found, useAskUserQuestionto confirm which one before continuing.
- IMPORTANT: if a specific semantic layer was not found, check for Saved Questions by running
call_tool({"name": "fa__list__saved_queries", "arguments": {}})to get a list of existing questions and descriptions saved on the Data Warehouse. Usecall_tool({"name": "fa__get__saved_query", "arguments": {"name": "<query_name>"}})to retrieve the SQL of a matching saved query, where<query_name>is thenamefield returned byfa__list__saved_queries.
Step 4 — Execute the Query
MANDATORY pre-query checklist — run for every query, no exceptions:
- Determine the schema from the domain routing table in Step 2: if the table is listed with an explicit schema prefix (e.g.
LOAN_OPS.LOAN), use that schema. OtherwiseFUND_ADMINis the default and most common schema.- Verify the table exists:
call_tool({"name": "dwh__list__tables", "arguments": {"schema": "<SCHEMA>"}})— use the schema from step 2. If the target table does not appear in the result, it does not exist — check the wrong→right table name reference in## SQL Compilation Safety Rulesbefore continuing. Do not query a table that is not listed.- Verify column names:
call_tool({"name": "dwh__get__table_schema", "arguments": {"table_name": "<TABLE>", "schema": "<SCHEMA>"}})— use the schema from step 2. Confirm every column you plan to SELECT or filter on appears in the schema with its exact name. Check the wrong→right column name reference in## SQL Compilation Safety Rulesif a column is missing.Then resolve any remaining uncertainty:
- Unclear intent — ask immediately. If the user's request contains a term that doesn't map to any known domain, table, or Carta concept in the Step 2 table, immediately call
AskUserQuestionwith focused options. Do not respond in prose first — go straight toAskUserQuestion.- Ask up to 2 clarifying questions. If, after checking saved queries (Step 3) and schema inspection, you still cannot identify the right table or domain, use
AskUserQuestionto ask the user at most 2 focused questions — e.g. fund-level vs company-level, metric type, entity name. After receiving answers, re-run Steps 2–3 before querying.Never assume a table or column name. Every wrong guess produces a Snowflake compilation error visible in production logs.
Use the MCP commands in sequence, substituting <SCHEMA> with the schema determined in the checklist above:
- Browse tables:
call_tool({"name": "dwh__list__tables", "arguments": {"schema": "<SCHEMA>"}}) - Inspect schema:
call_tool({"name": "dwh__get__table_schema", "arguments": {"table_name": "<TABLE>", "schema": "<SCHEMA>"}}) - Run the query:
call_tool({"name": "dwh__execute__query", "arguments": {"sql": "..."}})
Output format: Present results as a markdown table. Use fund or company names as row headers — never raw UUIDs. Currency values use $X,XXX format with commas; percentages use X.XX%. Bold totals and summary rows.
General Query Rules
- Always include LIMIT — default
LIMIT 200; use 50–500 for aggregations - Only SELECT — no INSERT, UPDATE, DELETE, or DDL
- Single SELECT only — no UNION / UNION ALL, no SHOW commands — the tool enforces one SELECT at a time;
SHOW TABLES LIKE '%...'and otherSHOW *commands also returnOnly a single SELECT statement is allowed. Usecall_tool({"name": "dwh__list__tables", ...})for table discovery and run separatecall_toolcalls when you need counts from multiple tables. - Do not query
INFORMATION_SCHEMA— it is not supported in this data warehouse and returns a hardValueError: Querying INFORMATION_SCHEMA is not allowed. Usecall_tool({"name": "dwh__list__tables", ...})to list tables andcall_tool({"name": "dwh__get__table_schema", ...})to inspect columns. These MCP tools are the only valid schema-discovery path. LATERAL(includingLATERAL FLATTEN) is not permitted — returnsValueError: Lateral is not permitted in query execution. To access keys in a VARIANT/ARRAY column, use explicit JSON path notation (e.g.col:key::STRING) rather thanLATERAL FLATTEN.- Date fields —
effective_dateforJOURNAL_ENTRIES;month_end_dateforMONTHLY_NAV_CALCULATIONS;investment_dateforAGGREGATE_INVESTMENTS - Deduplication — for
MONTHLY_NAV_CALCULATIONSandAGGREGATE_FUND_METRICS, useQUALIFY ROW_NUMBER() OVER (PARTITION BY fund_uuid ORDER BY last_refreshed_at DESC) = 1 - ALLOCATIONS has multiple rows per fund — always
GROUP BY fund_uuidwithMAX(fund_name)when using it for fund metadata
SQL Compilation Safety Rules
- Always schema-qualify tables:
FUND_ADMIN.TABLE_NAME(orLOAN_OPS.TABLE_NAMEfor loans). A bare name defaults toPUBLICwhere no customer tables exist. - Only query schemas visible in
dwh__list__tables: never query a schema that does not appear in that tool's output — unrecognized schemas are either internal-only or non-existent and will always fail. dwh__execute__querydoes NOT accept aschemaargument — the schema is encoded directly in the SQL asSCHEMA.TABLE_NAME. Never pass"schema"inside theargumentsdict.set_contexttakesfirm_idas a UUID string — pass the UUID value returned bylist_contexts, not a bare integer.- Use
fund_uuid(VARCHAR), notfund_id— the integerfund_idis internal-only and not available in customer-facing views. - Snowflake syntax only:
LIMIT NnotFETCH FIRST N ROWS ONLY;LIKE/RLIKEnotSIMILAR TO;ROW_NUMBER() OVER (...)not bareROW();DATE_TRUNCnotROUNDon dates; UUID values are strings (fund_uuid = '<uuid>'). - Wrong → right table names — if the user or context uses any name on the left, use the right instead:
- Wrong column names: Domain-specific corrections are in each semantic layer file's
⚠️ Common Mistakessection. Always rundwh__get__table_schemato verify column names before querying. Cross-domain shortcuts that frequently produceinvalid identifiererrors:
DWH Tool Invocations — Exact Forms Required
Use call_tool with these exact double-underscore names. Any other form (colon syntax, single underscores, direct tool invocations) returns NotFoundError: Unknown tool.
dwh__execute__querykey issql(notquery).dwh__execute__questionkeys:question(required). Do not passsql,fund_uuid,firm_uuid,format, or any other key.- JSON keys with spaces in VARIANT columns —
col:'Key With Spaces'andcol["Key With Spaces"]both fail withSQL compilation error. For keys containing spaces, use escaped inner quotes:col:'"Key With Spaces"'::STRING. This applies toAGGREGATE_INVESTMENTS.TAGS_JSONand any other VARIANT column with spaced key names. ORDER BYwithSELECT DISTINCT— columns used inORDER BYmust also appear in theSELECTlist when usingDISTINCT; otherwise Snowflake raisesis not a valid order by expression.
Deep Links
include_links: true required when users wants a direct link to the app. It adds a _links entry to each row for supported entity UUID columns:
include_links adds a _links entry to each row for supported entity UUID columns. Supported fields and their requirements:
Always include the resolver column(s) in every SELECT — _links is silently empty without them:
- Include
fund_uuidwhenever the result containsjournal_entry_gluuid,journal_entry_line_id,asset_id, orpartner_interest_group_id - Include
fund_uuidorfirm_carta_idwhenever the result containsentity_link_idorissuer_entity_link_id - Include these columns even when you don't display them to the user
Using _links: when a row has a _links entry, hyperlink the entity's display name (fund name, company name, LP name) to row["_links"][field]["web_url"]. Use the value verbatim — never reconstruct or guess URLs.
General Presentation Rules
Each semantic file's ## Presentation section is the source of truth for its domain. When a semantic file does not specify, fall back to these defaults:
- Render results as a markdown table with clear column headers
- Use names, never raw UUIDs as row identifiers — fund name, company name, LP name
- Currency —
$X,XXXwith commas; negatives/outflows in parentheses($X,XXX); bold totals**$X,XXX** - Percentages —
X.XX% - Multiples —
X.XXx(e.g. MOIC, TVPI, DPI) - Missing values — show
—rather than0ornullto avoid implying a real zero - Use Carta voice — "your fund's NAV", "your portfolio", not "query results"

