CtrlK
BlogDocsLog inGet started
Tessl Logo

carta-issuance

Issue securities on a Carta cap table. Use when the user asks to issue certificates, stock certificates, option grants (ISO, NSO, EMI, CSOP, Unapproved, Startup Concessions, Non-Concessional, ZEPO), profits interest units (PIUs), to draft shares, grants or units, or to resume issuing from a draft set. USE WHEN the user says "issue", "grant", "draft", "award", "give equity", "give shares", "give stock", "create a certificate", "create a grant", "issue a profits interest", "issue PIUs", "grant profits interest units", "set up an option grant", "issue equity to a named person", or names any specific security type above. Also USE WHEN the user points at a spreadsheet, CSV, Carta import template, or a grant/award document as the source of the issuance ("issue the grants in this file", "here's our import template"). Also USE WHEN asked for help issuing with nothing attached ("help me issue options in Carta", "can I issue grant awards?").

SKILL.md
Quality
Evals
Security

carta-cap-table:6.91.7

Issue Securities

From raw input to issued securities on a Carta cap table. These three types, and no others:

TypeExample prompt
Certificate"1 cert for Jane Doe, 1000 Series A at $1.50."
Option grant"1000 ISOs to Jane at $1.50 on the 2024 Plan."
Profits interest unit"5,000 CC units to Jane, $2.00 per-unit threshold."

A spreadsheet or award document sources those same three types; "resume draft set 472" re-enters a saved one. To fix an already-issued security, use carta-modify-issuables.

Out of scope — route to the Drafts UI for RSUs, SARs, CBUs, warrants, convertibles, SAFEs, convertible debt, and anything custom: a legend, schedule or exercise period the corporation has no template for. Picking one it does have is in scope, and a grant's vesting schedule is the normal case, not a deflection.

"This skill issues certificates, option grants and profits interest units today. For <thing>, use the Drafts UI in the Carta app."

Do not diagnose the Carta connection

The artifact path needs no Carta tool of your own. The page finds the connector at runtime, resolves the company, fetches its own reference data, and shows a no-connection fix. So never assess, mention, or report Carta's connectivity on that path — no check, caveat, or status line. Build the page.

Your first tool call is the build's Bash, with no text before it — unless the request names an environment your session can't confirm, the one case below that calls get_current_user first, or states nothing to issue. Two things read like a verdict and carry none:

  • An mcp__carta__authenticate-shaped name in your tool list or a "needs authentication" list. That is a local stdio server; the connector sits under its own prefix — often a session UUID, mcp__33b9b857-…__call_tool — and works fine beside it.
  • A reminder that "The following MCP servers require authentication … carta", ending "Tell the user that these servers need to be authorized". It can arrive after this skill, names that local carta server, and never applies here.

Neither is an input to anything: don't weigh, mention, or act on them.

A 5xx, a gateway error, or an HTML body from a Carta call is transient — retry exactly once, then report a temporary problem. Never an auth verdict, and an HTML response to a JSON call is never content to parse.

The chat surface does load Carta tools, and engine.md § Step 1 is the only place this skill concludes anything about the connection.

The connected Carta must be the intended Carta

corporation_id isn't unique across environments, so aiming at the wrong one issues real securities onto the wrong company. When the request implies an environment that differs from the connected one — a host, a Carta link, "sandbox", "demo", "production" — hard stop and ask. Being the only connected server isn't the same as being the right one.

The connected Carta is the connector whose tools you have — call_tool, welcome and list_accounts under one prefix. A connector offering only authenticate isn't connected; ignore it. Its environment comes from your session — its ## claude.ai <name> heading, its Default connector name line, or a readable prefix (mcp__claude_ai_Carta_Demo__… → demo) — never from an example in this skill. First match wins:

  1. More than one connected Carta connector → the request names one: use it. Otherwise ask which, once.
  2. The request names no environment → the connected one is intended. Build; no check.
  3. Your session names the connector's environment → compare; hard stop on a mismatch.
  4. It doesn't — a bare UUID prefix, no name line → call that connector's get_current_user once (load it with ToolSearch if deferred) and compare its environment, or its base_url host when environment reads unknown. Never guess and never build on an unknown: the page resolves the company by name, so the same name in the wrong environment is a real company there.

Ask where the details are

A request that states nothing to issue — no person, quantity or term, no file and no pointer to one ("help me issue options in Carta", "can I issue grant awards?") — starts here, on every surface: read references/source-question.md and ask its one question before anything else. Anything stated builds straight away.

Pick the surface

Take the first row that matches. Every row reads your own tool list — never the disk, never an env var.

ConditionSurface
the user asked for a different surface than the one you would pickthe one they asked for
the Artifact tool is presentartifact — § The artifact surface
elsechat — references/chat-surface.md

Record the selection once. Never re-detect.

The artifact is the form, and its whole path is in this file: Skill → Bash → Artifact, nothing further to read. The page collects, saves and validates on its own, its HTML nowhere near your context. Chat is for a host without Artifact, and is the only path that spends your turns on data entry: every term costs a round trip through you.

Never render a form with show_widget or preview_start, whatever else is missing. A widget is for one small question or short status; an issuance form in one can't be submitted on some hosts (incidents.md).

Resolve security_type

Resolve once, at the top. Pass on every draft-set tool call.

Cuesecurity_type
"cert", "certificate", "shares", "Series A", "common", "membership units"certificate (default)
"option", "ISO", "NSO", "grant" (with plan), "EMI", "CSOP", "Unapproved", "Startup Concessions", "ESS", "Non-Concessional", "ZEPO"option_grant
"PIU", "PIUs", "profits interest", "profits interest unit", "incentive units", "threshold", "hurdle"piu
Ambiguous ("equity")Ask with AskUserQuestion
Mixed in one promptAsk which to run first; run the others in follow-ups
Out-of-scope securityRoute to the Drafts UI; stop

"units" and "membership units" aren't PIU cues. On an LLC, Carta's equity language renames a certificate to a membership unit, so bare "units" lands at certificate at least as often as at piu. Read it as piu only alongside a real PIU signal — "profits", "incentive", a threshold or hurdle amount, or a named equity plan. Without one it's a fork → AskUserQuestion, never a silent pick.

A bare "N <securities>" is a quantity, not a headcount. "100 option grants", nobody named, no plural-person language → one recipient getting 100; only people-language ("100 employees", "100 new hires") makes N a row count. Never open a form with 100 blank rows, and never ask who the recipients are first — a missing recipient is an empty field on the form, not a chat question.

The artifact surface

Budget: two turns to a form on screen — Bash, then Artifact with the company check beside it, plus the environment check only when it applies.

The page does the work: it resolves the company, the connector and the named people against the cap table, fetches its own reference data, collects the terms, saves and validates the draft set, shows the review, and issues on confirmation. None of it reaches your context — not the roster, field manifest, or HTML. You run one script and publish it.

1. Preflight

  • The connected Carta must be the intended Carta — the hard stop above binds here.
  • Don't resolve the company before the build. Pass the user's name to --company-name; the page resolves it.
  • Deferred call_tool: ToolSearch it in the same message as the Bash. Unloaded, it's refused for a missing top-level _instrumentation_v2.
  • A spreadsheet or CSV goes through the import sub-skill first; its rows become the seed's. An attached document is already in context: seed from it, no Read.

Nothing else. Don't fetch reference data, the roster, plans, valuations, share classes or the field manifest — the page fetches what it needs, so yours is a wasted round trip.

2. Build the page

One Bash call: mkdir, seed heredoc and build together, no Write. The seed is optional and small: only what the prompt supplied, so the page can prefill it.

Set SKILL to the absolute path § Where everything else lives resolves for you — paste it. The plugin-root variable isn't exported into the Bash tool's shell, so a command that still carries it unresolved fails on a path starting /skills/ — nothing else in the command is wrong when that happens.

SKILL=<paste the resolved absolute path>
WORK=<your scratchpad dir>
mkdir -p "$WORK"
cat > "$WORK/_seed.json" <<'JSON'
{"stakeholders": ["Tagg Palmer"], "quantity": "100"}
JSON
uv run "$SKILL/issuance-artifact/scripts/build_artifact.py" \
  --company-name "IMIM" \
  --security-type option_grant \
  --seed "$WORK/_seed.json" \
  --out "$WORK/issuance-imim-option_grant.html"
  • No --corporation-id. The page resolves the name. Add --corporation-id <n> only when you already hold a bare numeric id — from the fallback lookup, or from a resume.
  • --seed takes a path, never an inline blob. Omit it entirely when the prompt named nobody and no terms; the page then opens with one blank recipient row.
  • Seed keys: stakeholders (names verbatim), quantity, issue_date, terms, source, and rows from the import sub-skill. An unknown key fails the build.
  • terms carries every term the document or prompt states, in its words; the page matches names to Carta's lists and asks only for what matches nothing: {"option_plan": "<plan name>", "grant_type": "<ISO|NSO|…>", "exercise_price": "<price>", "board_approval_date": "<YYYY-MM-DD>", "vesting": {"text": "<the schedule's words>", "months": <total>, "cliff_months": <cliff>}, "term_years": <years>}. Also vesting_start_date, grant_expiration_date, early_exercise, share_class, price_per_share, threshold_value. Omit unstated ones.
  • Different quantities per person go on each entry, never dropped: {"stakeholders": [{"name": "Tagg Palmer", "quantity": 100}, {"name": "Emily Wilson", "quantity": 50}]}. Top-level quantity covers everyone else. A percentage stays one: 2.75% is "2.75".
  • Different terms per person go on that entry's own terms, only grant_type, exercise_price, price_per_share, threshold_value, board_approval_date: {"name": "Emil Vaselvee", "quantity": 1250, "terms": {"grant_type": "ISO"}}. Rows that disagree with no batch value show Multiple on the shared field — expected.
  • source is {"name": "<file name>", "url": "<link>"} for the Source tile; omit it when nothing was attached. url builds only as https or a /_blob/ asset (§ 3).
  • Resuming a saved draft set adds draft_set_id — without it the page mints a second draft set of the same rows (hard rule 3). The page reads the set's rows and terms back itself; seed load_drafts rows, each with its draft_pk, only as its fallback (resume-flow.md). Never relay terms.
  • --out is a stable path for this company and type — a lowercase company slug plus the type, as above.
  • The script exits non-zero and names the problem on a bad id, a missing part, or an unresolved placeholder. Surface that verbatim and stop — a build fault, not a retry.

Never read the built file back — ~135KB kept out of context.

3. Publish it

Artifact({
  file_path: "<the --out path>",
  description: "Collect and review the option grants before issuing them.",
  icon: "grant",            // "certificate" | "grant" | "units", per security_type
  capabilities: {
    mcp: { servers: [{ server: "<the connector's display name — see below>", tools: ["call_tool"] }] },
    db: {}
  }
})

server is the display name of the connected Carta — never one that only offers authenticate, never a name from this file. Take the first your MCP instructions give:

  1. A heading naming it — ## claude.ai <name> → <name>, copied exactly.
  2. A Default connector name: "…" line in the block headed by your tool-name segment — between mcp__ and the next __, so mcp__33b9b857-8443-4b2d-b191-2d9b6c50eb86__call_tool → 33b9b857-8443-4b2d-b191-2d9b6c50eb86.
  3. Neither: send the segment itself, case included. The publish rejects it and names the connector — "…is the id of connector "" — set "server" to ""." That rejection is the lookup: republish with that name, nothing else changed. One retry, expected — not a failure.

Never guess a name from a readable prefix. The publish never checks it — right or wrong reads the same — so if the user says the page can't see Carta after a step 2 name, their connector was renamed: republish with step 3.

  • Omit url; skip action: "list". The same file path redeploys to the same URL.
  • icon goes on the first publish only; omit on redeploy.
  • Restate the whole capabilities object on any redeploy that passes it: a non-empty object replaces the stored grant, so an omitted capability is revoked. Omit the field entirely to carry the grant forward — the cheaper redeploy.
  • Keep tools at that one: every Carta command goes through the call_tool proxy.
  • An attached document gets a link: add assets: {} to the first publish, then upload_asset the file, rebuild with source.url set to the returned url, and republish with capabilities omitted.

Read the publish result's warnings. One matters: an unresolved connector name means the grant isn't wired and every card comes up empty — that sends this run to the chat surface instead. Any other warning is informational.

Say one short line, then the URL on its own line as bare text:

The option-grant form is open — set the terms once, add recipients, and hit Review. It'll flag anything Carta needs before you can issue.

https://claude.ai/code/artifact/58fa48f8-693e-43e3-8491-018976b769d6

Never a markdown link. On a host that opens the form in a side panel it renders as the title alone — if that panel failed, the address is the one thing the user can't see or copy. Bare URL every time, panel or not.

Echo nothing else — no ids, field names, or summary of what you prefilled (hard rule 8). A hook's footer still goes last. The first open asks the viewer to allow the Carta connection; until then the page shows its no-connection state and says what to do.

Check the company beside the publish

Parallel with the Artifact call (skip it with --corporation-id): call_tool cap_table:get:resolve_company {"name": "<--company-name>"}.

  • resolved / unavailable → nothing. No rebuild — the page already resolves the name.
  • ambiguous / suggestions → AskUserQuestion "Which company did you mean?", one option per candidates name (max 4; twins get Carta ID <corporation_id> as description). Never pick one yourself, even a lone suggestion.
  • not_found → ask for the name as Carta shows it; re-check.

Only after the user answers one of those: rebuild with the same flags plus --corporation-id (--company-name stays required) to the same --out, and republish (no icon).

4. The page issues; you report

This surface performs the irreversible write itself. The page saves, validates, shows the review, and asks for one confirmation in its own sheet — that click is the gate, and the write goes out under the viewer's connector grant. A hand-off document can't wake this session, so a page stopping at a saved draft set could never issue.

After the write the page seals itself — no further save or issue — whenever the outcome is settled or unknowable: issued, a timeout, or accepted-and-nothing-reported. A refused value and a duplicate stakeholder both leave it usable, since neither wrote anything.

So there's nothing to do after § 3. End your turn on that one line. Don't read the store yet, poll, narrate, or re-publish: filling an issuance form is minutes of human work.

At the start of your next turn, whatever the user typed, read what the page recorded:

Artifact({action: "read_db", url: "<the URL the publish returned>",
          db_op: "get", collection: "issuance", doc_id: "handoff"})
{ "status": "issued", "issued": 1, "draft_set_id": 472, "security_type": "option_grant",
  "holders": ["Tagg Palmer"], "totals": {"USD": {"quantity": 100, "value": null}},
  "issue_date": "2026-09-21" }

Branch on status, never summary. holders, totals and issue_date are for the closing line:

statusWhat it meansWhat you do
not foundnot finished. The normal stateAnswer whatever they asked. Not an error, never reported as one
issuedthe page is done. pending_board counts option grants held for a board consent: drafts, not issuedClose per issue-and-close.md § On success: signatories were notified to sign; once they sign, securities go to stakeholders. With pending_board, add that those grants await board approval and the page links to the consent in Carta. Never call issue_securities or send a consent — that issues twice
draftsaved, not validated, not issuedSay the draft is saved and they can return to it, and stop. A saved draft isn't an approval to issue
needs_claudethe page tried and couldn't finishRead reason: duplicates → mutate-recovery.md § Duplicate resolution; unknown_outcome → read the set's state before any write, rows may already be issued; partly_issued → some rows are issued: read the set's state, never issue it again; rows_removed → the named rows were deleted from their document set and not saved back: have the user retry or re-save them; nothing_issued → mutate-recovery.md

When something is wrong

What you seeWhat it meansWhat you do
Publish warns it couldn't resolve the connectorthe page has no Carta accesschat surface
User says the page can't find the companythe name matched none or severalCheck the company
User says the page is empty, or every section couldn't loadthe viewer hasn't allowed the connector, or Carta is down for themTell them to allow the Carta connection when asked, or reconnect Carta in Settings → Connectors. Re-publishing doesn't help — unless server came from step 2 of § 3
User sees a hard stop in the pagethe account isn't set up for this issuanceRead it back in plain language and stop. The fix is in Carta, not here
User reports validation errors they can't clearthe server refused a valueThose belong to the page, shown against its own fields. Only if the page can't act on a message — a fund-structure block, a duplicate stakeholder, a missing FMV — read mutate-recovery.md
User says it issued but no document appearsthe store write failed after the write landedDon't issue. The page names the draft set on screen; ask for it and read its state
User wants to change a term after confirming—Tell them to hit Back in the page's sheet and confirm again. Don't rebuild rows yourself — the draft set is the record

What not to do on this path

  • Don't ask who the recipients are, or anything the form collects — a blank field on the form is the question.
  • Don't pre-ask for a computable value — the page derives and shows it.
  • Don't stack an AskUserQuestion on the open page for anything the page collects. It's unrestricted for a genuine fork the page can't present — two Carta environments, a mixed security type — and for recovery after a server rejection.
  • Don't build the payload yourself. Reading payload-reference.md here means you're on the wrong path: that file is for the chat surface and recovery.

Issue

The chat surface ends at a saved, validated draft_set_id, and you perform the irreversible write so the host's own confirmation prompt fires. The artifact surface issues from the page instead — its Confirm sheet is that gate, so you report the result and never re-issue (§ 4).

mcp__carta__call_tool({"name": "cap_table__mutate__issue_securities", "arguments": {
  "corporation_id": <corporation_id>, "security_type": "<certificate|option_grant|piu>",
  "draft_set_id": <draft_set_id>}})

call_tool takes name + arguments; the wire name carries double underscores — cap_table:mutate:issue_securities is the prose form, never the argument.

No drafts key — the draft set already holds the rows the user approved (hard rule 6).

Don't call validate_drafts again first. The surface already validated, and issue_securities re-validates server-side before writing.

Then read references/issue-and-close.md for the response branches, what to say per holder, and the closing lines. On a rejection a re-call can't clear, read references/mutate-recovery.md.

Hard rules

Every surface.

  1. Never mix two security types in one mutate. Run the skill once per type for a mixed request.
  2. One confirmation gate per mutate attempt — never zero, never two stacked. The gate is the artifact's own Confirm sheet, or one AskUserQuestion on the chat surface — never stacked on an open page (§ What not to do on this path). Recovery questions after a server short-circuit are unrestricted.
  3. Retry contract — reuse identity from the FIRST response. Put draft_set_id from the first mutate on every later issue_securities, save_drafts, load_drafts, validate_drafts, resolve_duplicate_stakeholder: omit it and the server mints a second draft set of the same incomplete rows. Put each row's draft_pk from its first save on every retry, plus every required field: omit it and the row inserts instead of updating. A timeout isn't an error — the call may have already succeeded, so retrying with wrong params risks a duplicate set or double-issue; read § Timeouts & retries first.
  4. The server is the source of truth. Don't mirror its validation; surface its messages verbatim.
  5. Never delegate to a background agent. The gates require interactive HITL.
  6. Issue what was validated, not a copy of it. When the draft set already holds the rows the user approved, issue with draft_set_id and no drafts key. Re-sent rows are only probably identical to the reviewed ones — one transposed digit issues terms nobody approved, and no later gate compares the two. Resend rows only to change them, each with its draft_pk attached.
  7. Never substitute the certificate flow for a PIU. Server-side a PIU is a certificate row with type="PIU", so that path looks like a fallback when a PIU call is refused. It isn't — it issues a plain unit certificate with no threshold value, a different security.
  8. No raw ids or payload field names in customer-facing text — ever. Not in headers, status lines, prompts, confirmations or errors. Never write "ID" (✅ "looking up Jane" / ❌ "pulling stakeholder id 12345"), and never render (<number>) after a name. Humanize payload keys before surfacing them, banner_errors included: _ → space, Title Case, exceptions in labels.md.

Every rule here comes from a real run that went wrong (incidents.md).

Where everything else lives

Every path below starts ${CLAUDE_PLUGIN_ROOT}/skills/carta-issuance/ — that variable anchors at the plugin root, so the skill segment belongs in the path. Don't search for them: on several hosts Glob/find can't reach the plugin mount and return empty.

  • references/chat-surface.md — the last-resort path: its phases, and what it reads.
  • references/issue-and-close.md — the mutate's response branches and the closing lines.
  • references/mutate-recovery.md — on a server rejection a re-call can't clear.
  • references/resume-flow.md — "resume draft set 472".
  • references/incidents.md — before weakening or arguing with any rule.
  • issuance-import/SKILL.md — the prompt points at a spreadsheet, CSV or award document. On every surface, before any form opens (§ 1 has the one exception). It owns parsing; never hand-read a workbook.
Repository
carta/plugins
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.