Issue Securities
Walk an admin from raw input to issued certificates, option grants, or profits interest units on a Carta cap table. Those three are the only security types this skill issues.
| Flow | Example prompts |
|---|---|
| Certificates | "Issue 1 cert for Jane Doe, 1000 Series A at $1.50." · "Draft 5 founder certs on Acme." |
| Option grants | "Issue 1000 ISOs to Jane at $1.50 on the 2024 Plan." · "Draft 10 ISOs for new hires." · "Issue an EMI grant — 2000 options at £0.50." |
| Profits interest units | "Issue 5,000 CC units to Jane with a $2.00 per-unit threshold." · "Grant a profits interest — 500 units, overall threshold $1M." · "Draft 5 PIUs on the 2024 Plan." |
| Spreadsheet / file | "Issue the grants in ~/Downloads/Q3-hires.xlsx." · "Here's our import template — issue these certs." · "Draft the grant in this signed award agreement." |
| Resume | "Resume draft set 472." · "Continue the 'Q2 hires' draft set." |
To fix an already-issued certificate, grant or unit, that's carta-modify-issuables,
not this skill.
Out of scope — stop and route to the Drafts UI in the Carta app for RSUs, SARs, CBUs, warrants, convertibles, SAFEs, convertible debt, and for custom legends, vesting, acceleration, or exercise periods:
"This skill issues certificates, option grants and profits interest units today. For <thing>, use the Drafts UI in the Carta app."
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 if a PIU call is refused — it isn't. It
issues a plain unit certificate with no threshold value, a different security.
Carta's own import template has sheets for several of those, so an uploaded workbook routinely contains rows this skill can't issue. Phase 0.25 skips them and reports the count — never reshape an RSU row into an option grant to make it fit.
Architecture — one engine, two surfaces
The transaction is identical everywhere; only the surface that collects and reviews it varies.
- The engine is this file: resolve
security_type→ fetch reference data → assemble rows →save_drafts/validate_drafts/issue_securities→ recovery. It never knows which surface is in play. - The adapter implements exactly three capabilities. Nothing else may branch on environment.
| Adapter | Selected when | collectConfig (0.5) |
showReview (2) |
confirm (2→3) |
|---|---|---|---|---|
| Cowork (primary, ~95% of usage) | preview_start absent |
one show_widget form |
chat markdown | one AskUserQuestion |
| Code | preview_start present |
render-panel config panel |
render-panel review panel |
the panel's Confirm & Issue button |
This file documents the Cowork path, since that is nearly all real usage. If Phase 0 Step 1 selects the Code adapter, read code-adapter.md — its §0 lists every point where that adapter diverges, and anything §0 does not mention behaves exactly as described here.
Read these two in parallel with the first Carta fetch, not before it —
Phase 0.5's issuance_init has no dependency on them, so
reading first only delays it:
- references/cowork-adapter.md — the form, the chat review, the confirm, and the authoritative per-block field list. Skip only if Step 1 selected Code.
- references/payload-reference.md — the authoritative field contract: types, formats, picklists, autofills, date quirks.
The type-specific row file waits for security_type. Read
option-grant-fields.md, or
certificate-fields.md, or
piu-fields.md once that resolves — exactly one, and never
before. Loading the wrong one is pure cost, and a grant run has no use for Rule 144.
Both paths end the same way: the issue_securities mutate (Phase 3).
The SDK's HITL prompt on that mutate is the final, irreversible gate — never the review gate.
Hard rules
The field contract lives in payload-reference.md. Read it before constructing any payload. No invented keys.
Never mix two security types' fields in one mutate. Run the skill once per type for a mixed request.
One confirmation gate per mutate attempt — never zero, never two stacked. On Cowork the gate is the
AskUserQuestionin cowork-adapter.md §3; on Code it is the panel button, and stacking a question on an open panel suspends its submit watcher so the click never lands. Recovery questions after a server short-circuit are unrestricted on both. The SDK's HITL prompt is not this gate.Retry contract — reuse identity from the FIRST response.
draft_set_idfrom the first mutate goes on every subsequentissue_securities,save_drafts,load_drafts,validate_drafts,resolve_duplicate_stakeholder; omitting it makes the server auto-create a second draft set with the same incomplete rows. Each row'sdraft_pkfrom its first save goes on every retry row alongside every required field; omittingdraft_pkinserts a new row instead of updating. A timeout is not an error — the call may have already succeeded server-side, so retrying with the wrong params risks a duplicate draft set or a double-issue. Read payload-reference.md § Timeouts & retries before retrying any mutate that timed out.The server is the source of truth. Don't mirror its validation; surface its messages verbatim.
Never delegate to a background agent. The gates require interactive HITL.
Templates only — no custom payloads for legends, vesting, acceleration, or exercise periods: "Custom <thing> isn't supported here. Save as draft and finish in the Drafts UI."
No id sniffing. Required values come from user input,
cap_table:get:stakeholderswithdetail=full, or a documented default — never scraped from another grant or certificate. If none of the three applies, ask viaAskUserQuestion.Pre-save assertion. Before any
save_draftsorissue_securitiescall, walk every row and confirm eachalwaysfield (per Row templates) holds a non-null value. If one is missing, recover before the call, in order: (a) the row template's documented default; (b) re-run the stakeholder lookup (detail=full) and re-stampissue_date_relationship/email/stakeholder_kind; (c)AskUserQuestion. This is load-bearing because both failure modes are silent:save_draftsaccepts incomplete rows without complaint, and at issue time a row withstakeholder_id=nullslips duplicate detection, creates zero securities, and still returns success. That the server accepts a row is therefore never a reason to send one — never offer to save past a missingalwaysfield, and never describe the server's tolerance to the user as an option (save-validate-flow.md § The assertion is not advisory).Never ask who the grantees are before opening the collection surface. A missing recipient is an empty field on the surface, never a chat question — this is the single most common way this skill goes wrong. Two sub-rules follow from it:
- A bare "N <securities>" is a quantity, not a headcount. "100 option grants",
"100 certificates" — the server's
quantityfield counts shares/options for one recipient. With no named people and no plural-person language, open one blank block withquantitypre-filled to N (knowns.rows = [{"quantity": "100"}]). - Only pre-render multiple blank blocks when the language counts people — "100
employees", "grants for 100 new hires", or an explicit list of names. Then build
knowns.rowsas that many empty dicts so the surface opens pre-sized. - Genuinely ambiguous (rare) →
AskUserQuestionwhich one. That is a real fork, not the forbidden "who are the grantees" question. - "Which file did you mean?" is also a real fork, not this forbidden question — but only when the prompt referenced a file and Phase 0.25's search found zero or several candidates, or the workbook has more than one importable sheet. Never use it to ask who is in the file, and never in place of parsing a path the prompt already gave.
- A bare "N <securities>" is a quantity, not a headcount. "100 option grants",
"100 certificates" — the server's
Issue what was validated, not a copy of it. When a draft set already holds the rows the user approved, Phase 3 issues with
draft_set_idand nodraftskey. Re-sending rows makes the issued payload merely probably identical to the reviewed one — a transposed digit anywhere in it issues terms nobody approved, and no later gate compares the two. Send rows again only to change them, and then with eachdraft_pkattached.Never build a collection surface you cannot prove you have the rules for. See Confirm this skill actually loaded — run that check before the surface, and stop rather than building one from partial context.
The incidents behind these rules — including the ones that look redundant — are in references/incidents.md. Read it before weakening any of them.
Confirm this skill actually loaded
A Skill invocation can return "Launching skill: carta-issuance" and inject no content.
That has happened: the run continued for a full turn on a reference file it had read directly,
built a config form from it, and shipped a form whose option-type control was hardcoded to
ISO/NSO instead of gated to the corp's jurisdiction — a UK or AU corp would have been offered
the wrong tax treatments. The form looked entirely plausible. Nothing failed.
A partial load fails confidently, so check for it rather than waiting to notice. Before building any collection surface, confirm you can answer all three from loaded content — not from memory, and not from a reference file you happened to open:
| # | Question | Where the answer lives |
|---|---|---|
| 1 | How many numbered items are in Hard rules, and what does the last one say? | this file — the answer is 12, and it is this check |
| 2 | Which three so_types does the corp's resolved jurisdiction allow, and what does a wrong "US" default show a UK company? |
Phase 0.5 |
| 3 | Which script builds the surface on this adapter, which --security-type values does it take, and what must you never do instead? |
Phase 0.5 |
Question 1 is self-verifying: rule 12 is this check, so a run that cannot name it is a run that never loaded this file. Answering "11" means the content is stale or partial — stop.
If any answer is missing, stop and say so. Do not build the surface, and do not reconstruct the rules from a reference file:
"The carta-issuance skill didn't load fully, so I don't have the issuance rules in front of me. Re-invoke it (or start a fresh message) and I'll pick this up from the top."
Re-invoking is cheap. A form built on missing rules issues real securities on the wrong tax treatment, and neither the review nor the server catches it.
Voice & defaults
- Explain anything the skill chose. Tag
(default),(from existing record), or(autofill — <so_type> rule)with a one-line explanation under the review. The review is the user's only chance to reject a default, so an unshown default is one they never got to see. - Silent defaults are computable; prompted fields aren't. If the skill can stamp it (today's date, the plan's grant term, an autofill rule), stamp it and surface it tagged. Never ask twice.
- Show the full text of legally binding values (e.g. the legend body), not just the template name.
- Dates display as
MM/DD/YYYYeverywhere the user sees them. Payload formats follow payload-reference.md. - Explain jargon on first use (Rule 144 date, Section 4(a)(2), INDIVIDUAL, legend).
- No raw ids or payload field names in customer-facing text — ever. Not in headers, status
lines, prompts, confirmations, or error renderings. Never write the word "ID"
(✅ "looking up Jane" / ❌ "pulling stakeholder id 12345"), and never render
(<number>)after a name. Translate payload keys before surfacing them, including when echoing serverbanner_errorsback: humanize mechanically (_→ space, Title Case), with the exceptions listed in references/labels.md.
Resolve security_type
Resolve once, at the top. Pass on every draft-set tool call.
| Cue | security_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 prompt | Ask which to run first; run the others in follow-ups |
| Out-of-scope security | Route to the Drafts UI; stop |
"units" and "membership units" are not 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 is a real
fork → AskUserQuestion, never a silent pick.
Phase 0 — Preflight
Four steps, in order, all before any user interaction and before gathering any input. Step
1 is free. Steps 2–4 are the only round trips this preflight may spend: one ToolSearch, one
connectivity check, one list_accounts. Phase 0.5 then spends exactly one more —
issuance_init, which carries the stakeholder lookup with it.
Step 1 — Detect the environment from the tool surface
Look at your own available tools. preview_start present → Code adapter;
preview_start absent → Cowork adapter. That is the whole test: free, synchronous, and
correct in both environments, because render-panel cannot work without preview_start.
Do not run a Bash probe, check the filesystem, or reason about what the environment "looks
like" — the tool list is ground truth. Record the selection once and reuse it for Phases 0.5
and 2. Never re-detect per surface, never drop from Code to Cowork because a panel seems slow,
and never attempt render-panel when preview_start is absent.
Step 2 — Load every tool in ONE ToolSearch call
ToolSearch: "select:mcp__carta__fetch,mcp__carta__mutate,mcp__carta__welcome,mcp__carta__list_accounts"
mcp__carta__ is the placeholder prefix
(Step 2a) — when the session's
Carta tools carry a different prefix, substitute it into the select: string; the names after
the prefix never change. Zero matches on the literal mcp__carta__ names means the wrong
prefix, not a disconnected server — re-check the session's tool list before treating it as the
Step 3 stop.
One call, four tools, the complete set for the run. mutate is loaded here, up front, so
Phase 2 never has to load it after the user confirms — that would be serial latency at the
worst possible moment. On the Cowork path, add mcp__visualize__show_widget to the same
select: list if it isn't already loaded.
Never call discover or search_tools in the hot path. Every command name is hardcoded
below; looking up a name you already know is a pure round trip. discover is a debugging aid.
Step 2a — Carta command names (hardcoded, never discovered)
Reads go through fetch, writes through mutate. The argument key is params, not
arguments, and command names are colon-separated:
mcp__carta__fetch({"command": "cap_table:get:<noun>", "params": {…}})
mcp__carta__mutate({"command": "cap_table:mutate:<noun>", "params": {…}})
mcp__carta__ is a placeholder — here, in every code block below, and in every reference
file. The real prefix is environment-dependent (mcp__carta-test__fetch, plugin-scoped and
UUID-suffixed connector forms all occur). Resolve it from the session's tool list and
substitute it everywhere; only the prefix varies — tool and command names never do. The one
exception: the frontmatter allowed-tools entries are literal grant patterns — never
substitute there.
| Purpose | Command | Tool |
|---|---|---|
Reference data for the collection surface, plus named stakeholders via stakeholder_names |
cap_table:get:issuance_init |
fetch |
Stakeholder lookup for a roster miss — pass names= for several, search= for exactly one |
cap_table:get:stakeholders |
fetch |
| Load an existing set's rows | cap_table:get:load_drafts |
fetch |
| List draft sets (resume by name) | cap_table:list:draft_sets |
fetch |
| Cap-table totals for context math — authorized, outstanding, fully diluted, ownership % | cap_table:get:cap_table_by_share_class |
fetch |
| Save rows, no validation | cap_table:mutate:save_drafts |
mutate |
| Validate a saved set | cap_table:mutate:validate_drafts |
mutate |
| Save + validate + dedupe + issue | cap_table:mutate:issue_securities |
mutate |
| Resolve flagged duplicates | cap_table:mutate:resolve_duplicate_stakeholder |
mutate |
The totals source has a breakdown-sounding name.
cap_table:get:cap_table_by_share_class—corporation_idalone — returns authorized, outstanding, fully diluted, and ownership %. Context math only (e.g. percent-of-fully-diluted for a grant), never a payload source. There is nocap_table:get:cap_table_summary— guessing it returns "Unknown command" — and the similar-soundingcap_table_summary_reportis a different plugin's report command, not a name here; the row above is this skill's totals source.
Go through fetch/mutate, not call_tool. The runtime's tool descriptions deprecate
fetch in favour of call_tool and discover in favour of search_tools. That notice is
known and deliberately not followed — do not "fix" the contradiction. fetch stays because
the per-command tools call_tool would target are excluded from tools/list (the mechanics
below); the discover half is moot here because every command name is hardcoded in the table
above, so discovery never runs (full story:
incidents.md § Round-trips).
Both fetch and mutate are pinned gateway tools: always
present in tools/list, reachable in one hop, with scope and staff checks enforced inside the
command executor. The double-underscore form (cap_table__mutate__issue_securities) is not a
typo for a command name — carta-mcp also generates one hidden tool per command by swapping :
for __ — but those are excluded from tools/list and reachable only via a
search_tools → call_tool round trip, which is the cost this skill's hot path exists to
avoid. When such a tool isn't visible to the session, that route returns "Unknown tool"
instead.
Never call set_context for a corporation-scoped command. Every command above takes
corporation_id as a direct param — pass it.
Step 3 — Confirm Carta MCP connectivity
The whole flow depends on the Carta MCP server. When it doesn't answer, classify the failure before reporting it — "not connected" and "Carta is briefly down" need opposite responses from the user, and telling someone to reconnect a connection that was fine is its own failure.
| Signal | Meaning | Do |
|---|---|---|
| No Carta MCP tool in the tool list at all | Genuinely not connected | Stop with the message below |
| A call fails with HTTP 5xx / 502 / 503 / a gateway or HTML error body / a timeout | Transient upstream — the server is connected and briefly unhealthy | Retry once, then stop with the temporary problem message |
Genuinely not connected — stop before gathering any input:
"I can't reach Carta — the Carta MCP server isn't connected. Connect the Carta MCP server and try again."
Transient upstream — retry the failed call exactly once. If the retry succeeds, continue the run normally and say nothing about it. If it fails again, stop:
"Carta is having a temporary problem on its end — the connection is fine. Give it a minute and try again."
The retry cap is one, and it is a hard cap. A second failure means waiting, not another
attempt: re-running the same call against a 502 cannot succeed, and repeated attempts are the
inner-loop thrash this skill's budgets exist to prevent. Do not vary the call to make a retry
look novel, do not fall back to a different tool or a discover/search_tools probe, and do not
treat an HTML error body as a data payload to parse — an HTML response to a JSON call is an
outage signal, never content.
Step 4 — Resolve the corporation by name
If the prompt named a company and you don't already have its corporation_id, call
list_accounts(search="<name>") — never an unfiltered list_accounts(), which returns a
truncated alphabetical page that may never reach the name you want. search is the tool's own
name lookup; don't substitute a discover guess for it. Only ask the user via
AskUserQuestion if search returns zero or several ambiguous matches.
Phase 0.25 — Ingest an uploaded file
Skip this phase entirely unless the prompt references a file. When it does, the file replaces the prompt as the source of the rows — everything downstream is unchanged. It does not add a path around any gate: Phase 1 still resolves, Phase 1.5 still saves and validates, Phase 2 still reviews, Phase 3 is still the only mutate.
Supported: .xlsx .xlsm .csv .tsv (deterministic) and .pdf .docx (text extraction —
see Documents). The sub-skill
issuance-import/SKILL.md owns the mechanics;
issuance-import/references/column-map.md is the
header vocabulary. Never hand-read a workbook — a column read by eye is how a quantity lands
in an exercise-price field.
Step 0 — Confirm you can actually run the parser
The parser is a local script, so this phase needs Bash(uv run *). Check your own tool
surface for Bash before promising an import — it is present on the Code adapter and is not
guaranteed on Cowork.
No Bash → do not hand-read the file. Reading a workbook by eye is the failure this whole phase exists to prevent, and offering it as a fallback would make the parser's guarantees optional. Say so and route to the feature built for this:
"I can't read spreadsheets in this session. Two options: import it directly in Carta's Drafts UI, which takes this same template — or paste the rows here as text and I'll set them up."
Pasted-as-text rows are fine: they arrive in the prompt, so the ordinary prompt-driven flow handles them with the user's own values in plain sight. That is different in kind from silently parsing a binary nobody can see.
Step 1 — Locate the file
Take the path straight from the prompt; a pasted ~/Downloads/… path is the norm. Only if the
user said "the attached file" with no path, list the likely directories (ls -t ~/Downloads,
~/Desktop, the cwd) and look for a supported extension recently modified. Zero or several
plausible matches → AskUserQuestion which one (allowed by Hard rule 10's file carve-out).
Bash(find *) is deliberately not granted to this skill — use ls.
Step 2 — Parse, before spending any round trip
uv run "${CLAUDE_PLUGIN_ROOT}/skills/carta-issuance/issuance-import/scripts/parse_upload.py" \
--file "<path>" --out-dir "$OUT_DIR"
This first run is deliberately without --reference: it costs nothing, and its output tells
you the two things Phase 0.5's fetches need — the security_type and the names in the file. It
prints SECURITY_TYPE=, ROW_COUNT=, and the paths it wrote.
- Exit 2 with
AMBIGUOUS:+CANDIDATES=[…]— the workbook has more than one importable sheet.AskUserQuestionwhich, then re-run with--sheet "<name>". Never merge two sheets into one batch (Hard rule 2) and never pick for the user. - Exit 2 with
ERROR:— nothing usable. Surface the message verbatim and fall back to the ordinary prompt-driven flow; do not guess at rows.
Reconcile security_type with the prompt. File and prompt disagreeing is a real fork →
AskUserQuestion. The file wins only when the prompt never said.
Step 3 — Fetch reference data (Phase 0.5's fetches, informed by the file)
Run Phase 0.5's fetches exactly as documented, with two
inputs now supplied by the file: issuance_init's security_type, and its stakeholder_names
covering the names the file contains rather than the names the prompt named. That keeps the
lookup bounded by the file's row count, never by roster size.
stakeholder_names takes the whole list at once, which is the only correct shape here — a
40-row sheet resolved through a concatenated search= matches nobody and would create 40
duplicate stakeholders on a real cap table. Pass the names as a list; never join them into a
search string.
The account-setup gate still applies. Having a parsed file in hand is not a reason to push past it: a corp with no option-grant document set cannot issue one, whether the rows came from a spreadsheet or from the prompt. Stop where the gate says to stop — the parsed rows cost nothing and the file is still there afterwards.
Step 4 — Re-run the parser to resolve names to ids
uv run "…/parse_upload.py" --file "<path>" [--sheet "<name>"] \
--reference "$OUT_DIR/_data.json" --out-dir "$OUT_DIR"
--reference is the same _data.json you just built for build_config.py. The parser matches
the file's free text against it — vesting schedule, acceleration terms, share class (by name
or prefix), legend (by code or name), document set, equity plan, and the roster — and
writes _import_knowns.json.
Unresolved is blank, never guessed. A cell matching nothing leaves its field unset with
an import_notes entry. There is no fuzzy matching, and do not add any: an almost-match on a
vesting schedule or share class issues genuinely wrong terms, and unlike a bad quantity the
server cannot catch it.
Step 5 — Merge into knowns and open the surface
_import_knowns.json holds {security_type, rows, equity_plan_id?, batch_errors?}. Its rows
are your knowns.rows — merge them in and continue into Phase 0.5 unchanged. The row count
comes from the file, so Hard rule 10's quantity-vs-headcount heuristic doesn't apply here (a
40-row sheet is unambiguously 40 blocks). Carry batch_errors through to the surface's
panel-level banner, and hold equity_plan_id for the first mutate only.
Each row may carry import_notes — [{field, raw_value, reason}], display-only. Two
obligations, both load-bearing:
- Show every note on the surface, against the field it's about. On Code,
build_config.pyrenders them as amber markers automatically. On Cowork, render them the same way in theshow_widgetform — see cowork-adapter.md §1. - Render a noted field with nothing selected, so the surface's own readiness check blocks
submission until the admin picks.
build_config.pydoes this for you. A marker alone is ignorable; the blocked button is what actually prevents a silent wrong issuance.
Strip import_notes before any mutate — same discipline as the review-only fields
(Build the mutate payload).
The server rejects unknown keys.
Step 6 — Say what happened, in one line, before the surface opens
Read _import_report.json and report totals — never silently drop a column or a row. A dropped
Exercise Price column is a wrong-priced grant the user has no way to notice.
"Read 38 rows from Q3-grants.xlsx. State of Residency and Employee ID aren't fields this flow sets, and 3 values I couldn't match are flagged in the form — everything else is filled in. Review and submit when ready."
Rows the file carried but this skill can't issue (RSUs, SARs, CBUs, warrants, RSAs, convertibles) are skipped by the parser with a reason. Name them and their count — they need the Drafts UI, and an admin who thinks a 40-row sheet issued 40 securities when it issued 37 has been misled.
Name the unmapped columns too, not just how many. unmapped_columns in the report holds the
header labels this skill has no field for — several
(cowork-adapter.md lists them: state of residency, state
exemption, employee id, cost center, job title, salary, convertible note) are dropped by design,
and a file supplying one is an admin who expects it to land. "2 columns I couldn't map" leaves
them believing it did. Say which: "State of Residency and Employee ID aren't fields this flow
sets — add them on the stakeholder record in Carta." Naming a column costs a few words; a
silently dropped value the admin deliberately filled in is data loss they find out about later.
Documents (.pdf / .docx)
The parser extracts text to _import_text.txt and stops — it writes no rows, because prose has
no fixed layout and a script guessing at it would guess silently.
Read the text, build the rows yourself in the parser's own row schema
(issuance-import/SKILL.md § Row schema), and give every
field you filled this way an import_notes entry with "confidence": "low" so it renders as
needs-confirmation. Then continue from Step 3.
Take only what the document states. A grant agreement rarely names a vesting template by the
company's own template name, so leave vesting_template_id unset rather than inferring it from
prose like "vests monthly over four years" — that is the fuzzy match this phase forbids, done by
hand. Empty text means a scanned image: the parser exits 2 saying so; route the admin to OCR it
or type the values in, and never infer values from a filename.
Phase 0.5 — Configure the issuance
Collect everything on one surface — every field, per stakeholder — so the user submits once
instead of answering a chain of questions, and so a single batch can carry genuinely different
terms for different people. This is the engine's collectConfig; the adapter from Step 1
decides what the surface is.
Fetch the reference data first, and issue every call below in ONE assistant turn. They have no dependencies on each other; serial fetches here are pure latency.
Stakeholder lookup — pass the people the prompt named as
stakeholder_nameson the sameissuance_initcall below. The server resolves them alongside the reference data in one round trip, and the result comes back as that payload'sstakeholderssection with the same shape as the standalonecap_table:get:stakeholderscommand. There is no separate stakeholder fetch here. If the prompt named nobody, pass no names at all: there is nobody to resolve yet, and Phase 1 resolves whatever names the user types into the form.Never put two people in one
search=.searchAND-s its whitespace-separated terms, so it matches one person only —search="Jane Doe"works,search="Jane Doe Bob Smith"asks for a single human matching all four terms and returns an empty list with a perfectly healthy200. Commas don't help; they're stripped before the terms are AND-ed. Usestakeholder_names(here) ornames=(Phase 1) for several people — never a concatenatedsearch.Reference data —
cap_table:get:issuance_initwith the activesecurity_type, plusstakeholder_nameswhen the prompt named people. One call returns every section the surface and Phase 1 need, each with the same{count, results}shape as its standalone command:- Option grant —
vesting_templates,acceleration_templates,document_sets,valuations_409a,international_valuations,option_plans. - Certificate —
certificate_share_classes,legends,vesting_templates,acceleration_templates(cert vesting is opt-in but needs the same two lists once opted in). - PIU —
certificate_share_classes(read as the unit classes: same endpoint and shape, so the section keeps that name and its fallback command),option_plans,vesting_templates,acceleration_templates,document_sets,draft_set_init. Nolegends, no valuations — a PIU has no legend and no exercise price. Threshold value types are not a section: they are the fixed pairUnit/Overall.draft_set_initcarries the issuer'sthresholdNoun("hurdle"on the growth-shares preset) andisLLC. - Both, only when
stakeholder_nameswas passed —stakeholders, already atdetail=full, carryingid,full_name,email,event_relationship, andkindper person.
Every section is fetched server-side in parallel, so adding
stakeholder_namescosts no extra wall-clock — it removes a round trip rather than adding one.Partial failure is non-fatal. A section that failed comes back
nulland is named in the top-levelerrorsarray ([{section, message}]); fall back to that section's individualcap_table:get:<section>command. An emptyerrorsmeans full success — use the payload directly. This is the only fallback path; the rest of this file just says "from theissuance_initpayload".Read each section under its own name. Never let one section's
count: 0stand in for another's. Exactly one count may stop the flow — the Account-setup gate below, ondocument_sets.countread under that name and no other. Every other count, zero included, never gates: the surface is built and opened regardless (Hard rule 10).- Option grant —
Account-setup gate (option grant and PIU)
Runs once, immediately after the issuance_init payload is read — before FMV, jurisdiction,
plan, or any surface work — and skipped entirely for certificate. Both adapters run it.
Read the count from its section under that exact name. A real run aborted a valid
issuance by reading acceleration_templates' zero as document_sets'
(incidents.md § Reading server data wrong).
count >= 1 always passes, and no other section's zero ever gates.
security_type |
Section | On count == 0 |
|---|---|---|
option_grant |
document_sets |
Hard stop — document_set_id is an always field on every grant row |
piu |
certificate_share_classes |
Hard stop — prefix is an always field, so the batch could never issue |
piu |
document_sets |
Soft — a PIU needs one only when the issuer's own properties demand it, and no MCP command exposes those. Build the surface without the Documents row and let validate_drafts decide (Hard rule 5) |
certificate |
— | skipped |
Hard stop, before building any surface:
"Your corporation doesn't have any option-grant document templates set up yet. Create one in the Carta app, then come back." "Your corporation doesn't have any unit classes set up yet. Create one in the Carta app, then come back."
Soft, one line alongside the surface:
"This company has no profits-interest document templates. Carta will reject the issuance if your company requires a grant agreement — set one up in the Carta app if it does."
A section that failed to fetch — null, absent, or not the documented {count, results}
shape — is a failed fetch, not count: 0. Run that section's own fallback command and
gate on its count; if the fallback errors too, surface its message verbatim and stop as a
fetch failure, never with a no-templates message.
Why stopping here doesn't break Hard rule 10. Rule 10 forbids asking for collectible
fields before the surface opens. These fields pick among existing records and cannot
create one, so for an always field zero records makes it unfillable from any surface and the
batch can never issue — an account-setup blocker, the same category as an unreachable Carta
MCP (Phase 0 Step 3), resolved in the Carta app rather than on this surface. For a
conditional field (PIU document sets) the blocker isn't certain, which is why that one is
soft.
The gate reads only the sections in the table above. It is not a "stop on any empty section" rule and must not be read as one.
PIU: check issuer eligibility (before building the surface)
Profits interests belong to LLCs and partnerships. Nothing server-side rejects a PIU on a
C-corp — not the draft-set views, not the validators — so this check exists only here. Read
draft_set_init.isLLC from the issuance_init payload:
true→ continue.false→ warn once withAskUserQuestionbefore collecting anything: "<Company> isn't set up as an LLC or partnership on Carta, and profits interests are normally issued by one. Continue anyway, or switch to certificates?" Continuing is the admin's call; the server will accept it either way.- absent or
null(the section failed, or the field is missing) → treat as unknown, not asfalse. Continue without the warning — never block on a failed fetch.
Option grant: resolve the FMV and the jurisdiction (before building the surface)
Skipped entirely for certificate and piu — neither has an exercise
…(truncated)