Call capture
State: to-be-approved. Deploy-verified against a live workspace: not yet. Treat Done when
below as the acceptance test and review cargo-ai cdk plan before deploying. Make no outcome claim
for this skill until it is approved.
The outcome
What your team learns on calls stops living in a recording nobody reopens. Every morning one agent runs, and three things land in your repository:
- The raw capture. A committed script pulls every call the recorder has finished processing and
writes one file per call into
cadence/log/raw/calls/. That is the permanent archive, and it is never edited or deleted. - The log entry. The agent reads each raw capture and writes
cadence/log/calls/<date>-<account>.md: what was said, the intel under fixed headings, the actions as checkboxes. - The context update. Where a claim has been heard twice — an objection, a competitor, a
signal — the agent promotes it into
context/at the repository root, citing both calls.
Then it opens one pull request and stops. A human merges, and the next cargo-ai cdk deploy syncs
context/ into the workspace context repository, which is where every other Cargo agent reads
before it acts. A correction made once on a call propagates to everything that reasons about your
market, through code review rather than through someone remembering to update a doc.
This is a scheduled CI workflow plus a hosted agent, collapsed into one declared resource. The
schedule, the credentials and the repository binding live in
infra/call-capture/agents/call-scribe.ts, and the instructions beside it in
call-scribe.prompt.ts, in the same project as everything else the workspace runs. harnessSlug: "claudeCode" is what buys the working tree: the output of a call is a diff
across a dozen markdown files, and only an agent with a checkout can produce one.
Three properties make it safe enough to run unattended:
- The collection is deterministic. The agent does not fetch calls.
scripts/collect/calls.tsdoes, the same way every morning, and the agent is told not to improvise that step. - The pull request is the gate. The agent has repository write access and no other write path. It cannot email anyone, cannot touch the CRM, and cannot merge itself.
- The repetition bar. A claim reaches
context/only on its second independent occurrence. One prospect's offhand remark stays in that call's log entry, where it is evidence; it does not become something the whole company believes.
Example
Every morning, pull yesterday's Gong calls into this repo as log entries and open one pull request with anything worth adding to our context.
Illustrative output, fictional records:
[call-capture] scribe 2026-10-06
Captured 5 · scribed fresh 5 · scribed from backfill 0 · pending 0
+ cadence/log/raw/calls/2026-10-05-northwind.md
+ cadence/log/calls/2026-10-05-northwind.md
+ ## Objections
+ - "We already pay for Globex, I can't justify a second tool" (Priya Raman, VP RevOps)
+ ## Actions
+ - [ ] Send the Globex migration one-pager to Priya
+ context/objection/already-have-globex.md
+ Heard on 2 calls: cadence/log/calls/2026-09-22-fabrikam.md,
+ cadence/log/calls/2026-10-05-northwind.md
Five calls became five log entries, and the Globex objection reached context/ only because a second
account raised it; a single mention would have stayed in its log entry.
Put it in your project
This folder is a worked example: real CDK resources written for some other company. The job is
to end up with the code your company would have written, in your project, and an agent does the
adapting. If the cargo-cdk skill is in your session it carries the long form of this; if not, this
is enough.
-
Install it — the CLI does the copy. From inside the CDK project,
cargo-ai cdk add cookbook/call-capturewrites this example toinfra/call-capture/(resources andscripts/) and this procedure to.claude/skills/call-capture/. No project yet?cargo-ai cdk init <dir> --cookbook call-capture && cd <dir> && npm installdoes both; this folder never ships a shell. If you are reading this from the project's.claude/skills/, the install already happened — start at step 2. On a CLI too old to haveadd, copy this folder in as a sibling of what is there by hand; everything below is unchanged. -
Reconcile it with what is already declared. If the project already has a GitHub connector or an agents folder, rewire the imports to the existing one and drop the copy; two resources with one slug is a collision at deploy. The knowledge layer needs no work: the scaffold already declares the repo's root
context/ininfra/context.ts, anddefineContextis a per-workspace singleton, which is why this folder ships none. Append nothing to.env.example: this folder's one credential is a workspace environment variable, not a deploy-time one, and step 3 creates it. -
Pick the recorder, or write one. Ask which recorder records the team's calls, unless the repo's own context or the workspace connectors already say — it is the one input here with no reliable lookup, and every later step depends on it. Nine ship:
npx tsx scripts/call-capture/collect/calls.ts --listprints them with what each wants for a credential. Set it asRECORDERinscripts/call-capture/collect/config.ts— a typed constant, so a slug that is not a recorder failsnpm run typecheckand names the nine that are. It is not an environment variable on the agent: a choice the compiler can check and a reviewer can see beats a string in a deployed spec, and because the harness clones this repository every morning, editing it takes effect on the next run rather than on the next deploy. What no compiler catches is a wrong-but-valid slug — it reads the wrong vendor's API successfully and reports a clean, empty run every morning — which is the other reason the choice belongs in a diff. Then put its key in the workspace, not in a file. A harness agent inherits every workspace environment variable, so the credential is one CLI call and nothing in this project names its value:export CALL_RECORDER_API_KEY=… # this shell only; nothing persists it cargo-ai workspaceManagement envVar create --key CALL_RECORDER_API_KEY --secret \ --description "Call recorder API key read by scripts/call-capture"Omitting
--valuereads the exported variable, which keeps the key out of argv and out of shell history;--secretencrypts it at rest and the API never returns it again. Rotation isenvVar update <uuid> --value …and reaches the next morning's run with no deploy at all. Asecret("CALL_RECORDER_API_KEY")in the agent's env is the trade to refuse here: it sends the deploying machine's value, so the key has to be in a shell or a.envfor every deploy, by whoever deploys, and a rotation only lands on a re-apply. Gong and Clari Copilot issue two values rather than one; that single variable holds<first>:<second>and the adapter splits it. On a recorder that does not ship, this step is a new file, not an edit. Writescripts/collect/recorders/<slug>.tsexporting one object that satisfies theRecordertype inscripts/collect/recorder.ts— aproviderslug pluslistReady(from, to),transcript(id)andnotes(id)— then register it inrecorders/index.ts. Follow the shipped adapter closest in shape:avoma.tsfor Bearer REST,gong.tsfor POST-with-a-body and a speaker id that needs joining,fireflies.tsfor GraphQL,granola.tsfor a list too thin to carry attendees. Nothing else moves: deduplication, account slugging, the internal-domain filter, the file format and the rolling window are the same whoever records your calls, and they live inrecorder.ts.references/providers.mdcarries the contract and the procedure;references/recorder-apis.mdcarries the endpoints and exact field names for all nine plus five more that do not ship, each with the reason. Whichever recorder you land on, verify its response shapes against the live API before you deploy — eight of the nine were written from vendor documentation and say so on every run, the field names around stable endpoints move, and a wrong one captures nothing while reporting a clean run. Then setINTERNAL_DOMAINin the sameconfig.tsto your own email domain, or every internal call is captured as a customer one. Those two constants are the whole configuration surface: everything else the collector needs is either derived or the one credential in the workspace. -
Adapt. Work the sections below in order: What should not change is what you argue back about (say what breaks, then do it if they still want it); What you can change is what you offer unprompted (nobody asks for a variant they do not know exists); What you will be asked is the floor, and you derive before you ask. If you are asking more than about four questions you have skipped lookups. Record what you changed and why under a
## Decisionssection in your copy of this file. -
Run the collector by hand once, then plan.
CALL_RECORDER_API_KEY=… npx tsx scripts/call-capture/collect/calls.ts --dry-runfirst: it exercises the list call and the readiness filter and prints what it would take, writing nothing. Read what it printed — real subjects, real dates, real account slugs — because that is the only thing separating a working adapter from one that will report a clean empty run every morning. Then drop--dry-run. If it does not produce raw files locally it will not produce them on a schedule, and that is far cheaper to find out now. Thennpm run check && cargo-ai cdk plan, show the diff, and deploy only on an explicit yes:cargo-ai cdk deploy. Nevercdk init --forceinto a non-empty directory. -
Verify. Walk Done when line by line and report each with evidence. Deployed cleanly and produced nothing is the normal failure — and the second normal failure is a pull request nobody can review, so read the first line before you call this done.
What you will be asked
Derive before you ask. An input with a lookup is looked up, not asked. Only the ones marked asked genuinely live in the operator's head.
| Input | Kind | How it is answered | Why it matters |
|---|---|---|---|
repository binding (infra/agents/call-scribe.ts) |
value | derived: leave repository, defaultBranch and connector unset and plan fills them from the git origin of the checkout, taking the GitHub connector from the project's own. cargo-ai cdk check prints what it resolved: confirm the line reads your repo and ./. |
This is the working tree the harness clones and the only place its output can land. An owner/name written by hand is the one value nobody notices is wrong until a pull request opens against a stranger's repository. |
recorder (RECORDER in scripts/collect/config.ts) |
value | asked, once derivation fails: check the repo's own context and cargo-ai connection connector list first, then ask which of the nine slugs (--list prints them) records the team's calls. A recorder that is not there is one new adapter file plus an entry, per references/providers.md. |
It is the one input with no lookup anywhere in the project, and everything downstream is shaped by it. A typed constant rather than an env var so a slug that is not a recorder fails to compile, and so the one thing no compiler catches — a plausible-but-wrong slug, which reads the wrong vendor's API successfully and reports a clean empty morning indefinitely — is at least in a diff someone reads. |
CALL_RECORDER_API_KEY |
env | asked: the recorder's API key, put in the workspace with cargo-ai workspaceManagement envVar create --key CALL_RECORDER_API_KEY --secret (step 3) and named in no file here. A harness agent inherits the workspace catalog, so nothing declares it. Gong and Clari Copilot issue two values; that one variable holds <first>:<second> and the adapter splits it. |
It is the collector's only credential, and the workspace is the only copy of it that outlives the session that set it. In the catalog it is read server-side on every run, so a rotation needs no deploy; as a secret() in the agent's env it is the deploying shell's value, so every deploy needs the key again and a rotation lands only on a re-apply. |
INTERNAL_DOMAIN (scripts/collect/config.ts) |
value | derived: your own email domain, which the workspace members' addresses already name | It is how an internal call is told from a customer one. Avoma's is_internal is false on every meeting in some workspaces, so it cannot be used; leave the placeholder and every standup is captured as an account. |
adapter state (scripts/collect/recorders/index.ts) |
value | derived: --list prints which adapters were verified against a live workspace (Avoma) and which were written from vendor docs (the other eight). Nothing to decide — it decides how hard you check step 5 before deploying. |
The contract is compiler-enforced, so a half-written adapter fails to build rather than half-working. Pointed at the wrong API it fails on the first request, which is loud; pointed at the right API with a stale field name it captures nothing and reports a clean empty run every morning. |
GitHub connector (infra/connectors/git.ts) |
value | derived: cargo-ai connection connector list shows whether one is authorized; if not, cargo-ai cdk add connector/github opens the OAuth consent. The declaration is default: true because a deploy cannot mint an OAuth grant. |
It is the agent's entire write path. Without it the run does the work and has nowhere to put it. |
LLM connector and model (infra/connectors/anthropic.ts) |
value | derived: cargo-ai connection connector list shows whether an Anthropic connector is authorized; if not, cargo-ai cdk add connector/anthropic takes the key. default: true because a deploy cannot mint one. Any Anthropic model pairs with claudeCode; the agent's languageModel is a placeholder to set. |
A harness does not bring its own model — it runs against Cargo's LLM proxy, so this is what the daily run is billed and metered against. Omit either and defineAgent throws at plan; pair claudeCode with an openAi connector and it typechecks green and fails at deploy. |
| cadence and context paths | value | derived: read cadence/README.md and context/README.md, and ls cadence/log/ for what already exists |
The agent writes into layers humans already curate. A second parallel folder splits the record in half and the repetition bar stops seeing the earlier occurrence. |
Checked before moving on, not after the deploy:
RECORDERinconfig.tsis the recorder the team actually records on —npm run typechecksettles whether the slug exists, nothing settles whether it is the right one but a reviewercargo-ai workspaceManagement envVar listshows aCALL_RECORDER_API_KEYentry, marked secret — that listing is the deployed credential, and a value only ever exported in a shell is not one- the collector was run by hand once and wrote real raw files
cargo-ai cdk checkprintsagent:call-scribe bound to <your repo>#<branch>with no trailing subdirectory — the repo is the one holdingcontext/andcadence/, and the GitHub grant can push to it. A trailingin infra/is the failure to catch here: it roots the harness where there is no node_modules, so the collector cannot run- exactly one
defineContextin the project, resolving to the repo's rootcontext/ scripts/call-capture/package.jsonis present in the project after the install
What you can change
The code is a worked example. These reshapes are expected, and the agent offers them rather than waiting to be asked. Every one costs something; that is what makes it a variation and not the default.
| Variation | When it is right | How | What it costs |
|---|---|---|---|
notes-not-transcripts |
The repo is growing faster than you want, or your recorder's AI notes are genuinely good | Reorder the fallback in collect/calls.ts so notes are fetched first and the transcript is the fallback |
You lose the verbatim record, so the scribe can no longer quote and nobody can check a summary against what was actually said. Notes are already an interpretation; scribing them is interpreting an interpretation |
raise-the-bar |
Your context is filling with claims that turn out to be one customer's opinion | Raise the repetition bar in infra/agents/call-scribe.prompt.ts from two independent occurrences to three, and require them to come from different accounts |
The knowledge layer lags the field by weeks. A real, fast-moving objection sits unwritten while it is costing you deals |
widen-the-cap |
The backfill queue is not draining — the pull request reports a remainder that never falls | Raise the per-run cap in infra/agents/call-scribe.prompt.ts, or leave it and let the daily runs grind through the queue |
The pull request stops being reviewable, which is the whole control. A diff nobody reads is an auto-merge with extra steps |
internal-calls-too |
You want deal reviews, onboardings and retros in the record as well as customer calls | Drop the external-attendee filter in collect/calls.ts and file internal captures under their own folder |
Volume roughly doubles and the signal thins: internal calls restate what customers said, so the repetition bar counts the same occurrence twice and promotes it as if two customers had said it |
two-recorders |
You are mid-migration between recorders, or sales records on one and CS on another | Run the collector once per recorder in step 1 of infra/agents/call-scribe.prompt.ts: --recorder=<a> then --recorder=<b>. Both write into the same archive under their own source: prefix |
Deduplication is per-recorder, because the ids are. A call both tools recorded is captured twice and scribed as two entries, and the repetition bar then counts one conversation as two occurrences and promotes it as if two customers had said it |
What should not change
However far you adapt, these hold. Ask for one anyway and the agent tells you what breaks, then does
it if you still want it, and records why under ## Decisions in your copy of this file.
- The agent does not fetch calls; the script does. (
scripts/collect/calls.ts) A fetch loop an agent re-derives every morning is a fetch loop that silently changes shape — a window that drifts, a filter that quietly widens, a field read differently on a day the model was less careful. The raw archive is the one thing here that has to be byte-identical in its rules every day, because everything downstream is diffed against it. Recorder.providerstays one field, and the registry key equals it. (scripts/collect/recorder.ts,scripts/collect/recorders/index.ts) It is written into everysource:line and compiled into the regex that reads those lines back. Split it into two literals, or file an adapter under a key that disagrees with its ownprovider, and the day they drift the collector stops recognising what it has already captured and re-captures the whole window every morning — producing files, never erroring, until someone notices the repository doubling in size.resolve()refuses the mismatch andevals/contract.mjsfails on it; leave both in place.- A recorder swap is a selection, and a new recorder is a new adapter — never an edit to the
pipeline. (
scripts/collect/recorder.ts) Deduplication, slugging, the internal-domain filter, the file format and the window are the same whoever records your calls. Move one of them into an adapter to make a vendor fit and the next adapter has to reimplement it, which is how two recorders start writing subtly different frontmatter and the dedup stops seeing half the archive. That includes the internal/external flags four of the recorders offer: they disagree with each other, and one of them is wrong in some workspaces, so every adapter passes attendees through and one domain rule decides. written: "live"is a claim someone made with evidence. (scripts/collect/recorders/index.ts) It means an adapter was run against a real workspace, and the collector prints a warning on every run of one that was not. Flip it to silence the warning and the registry starts implying that nine recorders are tested when one is — which is exactly how a convenience becomes a promise nobody checked, on a pipeline whose failure mode is a clean empty run.- Choices stay in code, the secret stays in the workspace, and the agent declares no
env. (scripts/collect/config.ts,infra/agents/call-scribe.ts) The recorder and the internal domain are typed constants, so the compiler checks the slug, a reviewer reads both in the diff, and an edit lands on the next clone rather than the next deploy. The credential is the mirror image: a workspace environment variable read server-side on every run, so it outlives the shell that set it and rotates without a deploy. Move a choice into the spec and nothing checks it; move the key there as asecret()and the next deploy is blocked on whoever runs it having the value exported, which on an unattended 07:00 pipeline is discovered as a morning with no pull request. scripts/call-capture/package.jsonstays. (scripts/package.json) It is not decoration. The CDK loader imports every.tsunder the project root except directories carrying apackage.json; delete it andcargo-ai cdk planimports the collector and runs it against the live API on every plan.- The harness root stays the directory holding the
package.jsonthat declares@cargo-ai/cdk. (infra/agents/call-scribe.ts) That is wherenode_modulesis, so it is the only placenpx tsx …/collect/calls.tsresolves — and in the scaffolded layout it is the repository root, which is also wherecadence/andcontext/live. The binder infers it, so nothing declares it here; what must not change is the outcome. Check it, do not assume it:cargo-ai cdk checkprints the resolved binding, and a line endingin infra/means the harness was rooted where there is no package.json and no node_modules. The collector then cannot run at all, and the morning reports clean and empty. On a CLI old enough to resolve it that way, pinrootDirectory: "."in the repository block until you upgrade. - The agent opens a pull request and never merges it. (
infra/agents/call-scribe.prompt.ts) Everything downstream readscontext/as fact. Remove the review gate and a hallucinated objection, a misheard number, or a vendor call misread as pipeline becomes what your scorer, your researcher and your SDR all believe — and nothing will ever contradict it, because they read the same file. - One entry per call, keyed on the provider uuid, and raw captures are never touched.
(
infra/agents/call-scribe.prompt.ts) The uuid in each file'ssource:line is the only idempotency key in this system. Let the agent rewrite entries "to improve them" and the run stops being idempotent: a re-run re-litigates history, the diff is unreviewable, and the earlier occurrence the repetition bar counted changes underneath it. - A claim reaches
context/only on its second independent occurrence. (infra/agents/call-scribe.prompt.ts) Drop the bar and the knowledge layer fills with singletons at the same confidence as durable truths. The failure is not noise, it is that nobody can tell which is which any more, and the layer stops being trusted at exactly the moment it is big enough to matter. - The collector's window overlaps the previous run. (
scripts/collect/calls.ts) Transcripts are not ready when a call ends. A one-day window silently drops every call the recorder was still processing at the cron minute, and because the window only moves forward, those calls are never seen again. The overlap is free: uuids already on disk are skipped before any transcript is fetched.
Done when
--dry-runlisted the calls it would take, and the run without it wrote those files intocadence/log/raw/calls/with asource:id in each; running it twice wrote nothing the second timeRECORDERinconfig.tsis the recorder the team records on, and the capturedsource:lines carry that slugcargo-ai workspaceManagement envVar listshowsCALL_RECORDER_API_KEYas a secret entry, andgrep -r CALL_RECORDER_API_KEYfinds its name in the collector and the docs and its value nowhere — the deployed run reads the catalog, so no local shell has to hold the key again- on an adapter marked
docsrather thanlive:--dry-runlisted real calls with the right dates and account slugs through it, and one captured file was opened and read — that is what moves it tolive, and until someone does it a clean run proves nothing - on a recorder that does not ship: the new adapter compiles (a half-written one does not typecheck,
and the error names the missing field), it is registered under a key equal to its own
provider,--dry-runlists real calls through it, and nothing inscripts/collect/recorder.tshad to change to make it fit cargo-ai cdk planreports three resources and does not hit the recorder's API while planning- the first scheduled run opened one pull request whose body reports four numbers: captured, scribed fresh, scribed from backfill, and pending remaining
- every scribed call has exactly one entry under
cadence/log/calls/, and no raw file was edited, moved or deleted - at least one
context/file in the diff cites two different calls as its occurrences, and no file in the diff cites only one - a call where your company was the buyer, not the seller, is filed as vendor intel and appears nowhere as pipeline
- after merging and
cargo-ai cdk deploy, the changedcontext/file is readable from the workspace context repository, and an agent with thecontextcapability quotes it back
What it costs
There are no connector actions in this skill, so it consumes no per-record credits: the collector
talks to your recorder directly from the harness environment, under whatever API limits your
recording plan already gives you. Those limits differ enough between recorders to be worth checking
once — a daily three-day window is nothing against Grain's per-minute allowance and is real against
Fireflies' Free tier of a few dozen requests a day, and references/recorder-apis.md records
each. The collector paces itself and backs off on a 429 regardless, so the failure is a slow run
rather than a half-captured one.
The recurring cost is the harness run itself, once a day, and it scales with how much the agent reads — which is the raw captures it scribes. The per-run cap is the cost control as well as the review control. A lower cap spreads the same backlog over more days at the same daily cost rather than paying for it all at once, and the pending remainder in the pull request body is how you watch whether it is keeping up.
Composes into
account-scoring and any agent with the context capability (they read the ICP and objection files
this keeps current), crm-enrichment (the account files this writes name the record the enrichment
fills), monitor-buying-signals (the signals this promotes are what a feed then watches for).
