* Stop documenting the first-party enrichment provider
Its playbook, its 21 priced actions, and every recipe step that led with it
are removed. Each one now leads with the rung the pack already named as its
fallback, so nothing here is invented — the replacements were sitting in
stage-action-map.md and alternatives.md the whole time.
The substitutions:
firmographics aiArk.enrichCompany (0.01) -> companyEnrich (0.25) -> waterfall (1)
person aiArk.enrichPerson (0.1) -> waterfall.enrichContact (2)
funding enrichCrm.getFunding (1), now the catalog's only one
technographics builtwith.getDomainSummary (free) -> enrichDomain (1)
dedupe free storage query against the workspace's own models
That last line is the one worth reading twice. The match actions existed to
mint an id the other actions required; every replacement keys on the domain
or a LinkedIn URL, so a step that cost 0.5/record now costs nothing.
The event feed has no replacement and the recipes say so. fetchBusinessEvents
took a timestamp_from and nothing else in the catalog does, so funding-watch,
re-engagement and lost-deal-revival are rewritten as a diff: re-pull, compare
against the stored round date, keep what moved. Cadence becomes the only cost
dial, which is why funding-watch's default drops from daily to weekly.
Every credit budget was recomputed rather than relabelled:
build-tam 2,955 -> 2,210 (500 companies + 1,500 contacts)
prospecting P2 821 -> 107 (200 verified prospects)
icp-discovery 602 -> 204 (200 deals)
context bootstrap 3 -> 1
Catalog counts now follow the generated data file rather than the older
hand-kept figure: 136 integrations, 176 credits-based actions of 513.
Also bumps the CLI pin 1.0.66 -> 1.0.78.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wkwbhrEoE5KW9A1CKBmcA
* Fix the three generated artifacts the removal invalidated
CI caught what the local lint could not: skills-lint.mjs is only one of five
checks, and the other four all had something to say.
llms.txt was stale — it is generated from the skill descriptions, and
cargo-gtm's dropped a provider from its roster.
The codex package build enforces override version == upstream version, so
bumping only .github/package-overrides/cargo-quickstart.SKILL.md to 1.0.3 broke
it. The guard exists to stop the override silently shipping last month's demo,
so the fix is to bump both, not to revert the override.
That build also patches literal source strings at package time, and one anchor
ran through a line this branch rewrote:
snitcher.searchSessions (0 cred) -> cargo.matchProspect
snitcher.searchSessions (0 cred) -> aiArk.reverseLookup (0.05)
The edit strips the visitor-de-anonymisation path from the packaged skill, so a
silent miss would have shipped it. Re-anchored, and every other PACKAGE_EDITS
find string verified against the tree — 1 broken, now 0.
Also drops the packaged cargo-connection description from 138 integrations to
136, matching the repo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wkwbhrEoE5KW9A1CKBmcA
* Say where revenue went, and that employee ratings went nowhere
Removing the provider took two data points off the map without replacing the
map entry, which is the failure mode this recipe's own feasibility gate exists
to prevent: an attribute that quietly stops being answerable reads the same as
one nobody asked for.
They are not the same case, so they get different rows.
Revenue and NAICS survive. companyEnrich.enrichByDomain (0.25) returns them in
the same call as firmographics, employees, funding and socials — so it fills
several rows at once and the row says that, because the per-account arithmetic
in step 4 gets it wrong otherwise.
Employee ratings have no successor at any price. Nothing credits-based in the
catalog returns Glassdoor-style employer reputation. The row says none, and
says to report it as a research note rather than substituting a scrape that
reads like the real thing — which is the recipe's stated position on valuable
attributes with no obtainable source, applied to itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wkwbhrEoE5KW9A1CKBmcA
* Fix the data-flow breaks the provider swap left behind
Bugbot found eight, all real, and verifying them turned up five more. The
pattern in every case is the same: I replaced an action but left the code that
consumed its output reading the old response shape, so the step still runs,
still bills, and quietly returns nothing.
Response-shape breaks:
funding-watch Pattern A still filtered funding_rounds[].announced_date
while Pattern B read lastFundingDate. The 90-day select
matched nothing, so the primary path returned an empty
list after a paid pull.
re-engagement Step 5 read .results[] and .events[] from a file step 3
now writes as a bare array. Funding branch always empty.
lost-deal-revival Step 4 read .events[0] and expected a deal_id that
getFunding does not return. Step 3b now carries deal_id
through, which is the only place it exists.
Wrong-source breaks:
re-engagement The diff read last_funding_round_at off the Contacts
segment. It is a Companies column, so the stored date was
always empty and every account re-flagged every week. Now
queries the Companies model, and fans the company-level
signal back out to contacts, since step 5 unions on email.
build-tam Step 4 keyed searchLeads on .linkedinId from the
aiArk.enrichCompany output. No enrichment action returns a
Sales Navigator accountId. Now carried from the sourcing
step, with an explicit gate for rows that have none.
guide snitcher.searchSessions identifies a COMPANY; I had
chained it into aiArk.reverseLookup, which needs an email
or phone. Its own playbook says to route identified
domains through the enrich chain.
Budget break:
prospecting P2 step 5 ran FullEnrich over every lead while the budget
quoted the residue. 200 credits against 60. P1 had the
gate; P2 did not.
Pre-existing, fixed because the fixes above sit inside them:
icp-discovery [.[] | {domain}] over storage query execute output, which
is {rows: [...]}. Enriched one null domain, not 200.
jq inputs[N] `inputs` is a generator, not an array, so the union
expressions in re-engagement and lost-deal-revival errored
out. Slurped to [inputs] as $in.
Every rewritten jq expression was run against fixtures rather than eyeballed.
Also five provider mentions the sweep missed, because they wrote it as bare
`cargo` with a price rather than cargo.actionName: societeInfo, snitcher,
enrichCrm, linkedin, oceanio, apolloio, rocketreach.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wkwbhrEoE5KW9A1CKBmcA
* Make funding-watch Pattern B stand on its own
It re-pulled two hardcoded example domains and then diffed them against
/tmp/targets.json, a Pattern A artifact. Run Pattern B alone and the slurp
fails; run both and it compares a fresh pull of one set against stored dates
for a different set, so the "new round" filter cannot fire correctly either way.
Both sides of a diff have to come from the same query. Now they do: one storage
read supplies the domains to pull *and* the stored dates to compare against, so
the sets align by construction and the pattern has no cross-dependency.
Also states what a missing stored date means — it compares against "" and always
passes, which is right on a first run and is exactly why the write-back step is
not optional. Without it every run is a first run and the watch re-flags the
whole segment forever.
Verified against fixtures: an account whose round moved is kept, one whose
stored date already matches is dropped, one with no stored date passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wkwbhrEoE5KW9A1CKBmcA
* Route the questions people ask but the map never answered (#124)
Measured instead of guessed. Of the 176 credits-based actions, 148 were
reachable from a routing surface — the stage map, the phase guides,
alternatives.md, a recipe. The other 28 were reachable only from their own
provider playbook, which is findable if you already know which provider to
open. That is the wrong way round.
The worst of it: headcount had no section at all. It is the most-asked company
attribute and the first row of the custom-datapoints feasibility gate, and not
one of the four salesNavigator.find* actions appeared anywhere in the stage map.
New section carries all four, plus the only historical series
(companyEnrich.getWorkforce), plus linkedin.findCustomHeadcount for the roles
the fixed buckets don't have, plus the companyId prerequisite that quietly
turns a 0.25 action into 0.30 on a list that arrived without one.
"Who works at this domain" was not a documented question either. It is not the
same as searching a population by title, and it has two answers at very
different prices. New section, with the trap stated: looping hunter.searchDomain
to build a list is the pitfall its own playbook warns about.
Also adds rocketreach, mixrank and societeInfo to Enrich/Sourcing —
societeInfo reaches French registry data (NAF code, collective agreement,
filed accounts) that nothing else in the catalog does — and gives Hiring intent
a second index at the same price.
154/176 now. The remaining 22 stay unrouted on purpose, and the CHANGELOG says
why for each: the four LinkedIn engagement actions would undercut the
acceptable-use refusal to batch-blast them, three natives belong to other
skills, searchLeadsLegacy costs 300x its replacement, and the rest are finer
grain on an already-routed surface rather than separate questions.
One thing the build caught: adding aiArk.analyzePersonality re-introduced a
term the codex package deliberately strips, since the directory rejected
person-level inference. Kept for the channels that carry it, with matching
PACKAGE_EDITS so the package still removes it. Dropped the forager rung rather
than add machinery for a last-resort action priced 20x above its own chain.
All 67 anchors verified.
Claude-Session: https://claude.ai/code/session_019wkwbhrEoE5KW9A1CKBmcA
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
12 KiB
Bootstrap workspace context from a domain
The prescriptive, automatable version of Phase 1 of lifecycle.md. Use this when the user wants to seed an empty (or thin) context repo from public data, starting from nothing more than their company's domain. The recipe enriches the company via aiArk + builtwith + enrichCrm + theirStack, scrapes public sources in parallel sub-agents, and writes one file per atomic concept through cargo-ai context runtime write — skipping any domain that already has content.
Output: a populated global/, icp/, persona/, client/, proof/, signal/ (and where evidence supports it, alternative/, objection/, insight/) — enough that a fresh agent session can hold a coherent conversation about the company. Phase 2 (call-driven refinement) is deliberately out of scope here — see the "What this recipe does NOT do" section.
Trigger phrases:
- "Set up my workspace context from acme.com."
- "Bootstrap the context repo — my domain is acme.com."
- "Fill in the ICP and personas from our website."
- "My workspace is empty, just use our domain to populate everything."
What this recipe exercises
cargo-ai context runtime browse/graph getfor the idempotency check.- Domain-keyed enrichments (
aiArk.enrichCompany,builtwith.getDomainSummary,enrichCrm.getFunding) for the factual spine. - Parallel sub-agents for public-source scraping (website, careers, blog, news, review sites).
- The driving agent's native LLM to synthesize each digest into typed markdown matching the per-domain template (no
cargo-ai orchestration action executedouble-hop — that pattern is for workflow node graphs, not for an agent already in the loop). cargo-ai context runtime writeto commit one file per concept.
Required inputs
Before executing, the agent needs:
domain(required) — canonical domain (acme.com), no protocol, no path.companyName(optional) — falls back to whateveraiArk.enrichCompanyreturns for the domain.depth(optional, defaultstandard) —minimal(global + 1 icp + 2 personas),standard(full domain coverage),deep(also scrapes G2/Capterra/Reddit/HN for objections + alternatives).
If domain is missing, ask once and stop. Don't guess from the user's email — workspace domain and user email often diverge.
Recipe
Step 1 — Confirm the target workspace
Each Cargo workspace maps to one company. runtime write pushes immediately. Wrong workspace = polluted repo for someone else.
cargo-ai whoami
# → user.email, workspace.uuid, workspace.name
Read back workspace.name to the user and confirm it matches the company the domain belongs to. Stop and ask if the name is generic ("Main", "Test", a person's name, an internal codename) — workspace names are user-set and frequently don't match the customer-facing brand.
Non-interactive mode (server-side trigger from signup, scheduled job, etc.): skip the read-back if domain was passed in at session start and workspace.uuid was pinned at login. The capture point at signup is the authority — don't add a blocking question that breaks the automation.
Step 2 — Idempotency check (the "if not exists" part)
Inventory what's already in the repo so we only fill gaps, never overwrite:
cargo-ai context runtime browse > /tmp/ctx-browse.json
cargo-ai context graph get > /tmp/ctx-graph.json
# Count entries per domain (excluding _template.md)
jq -r '.files[] | select(.path | test("^[^/]+/[^_].*\\.md$")) | (.path | split("/")[0])' /tmp/ctx-browse.json \
| sort | uniq -c
Build a skip-list: any domain (global/, icp/, etc.) with ≥ 2 non-template entries is considered "already seeded" — leave it alone. Print the skip-list to the user before any writes so they see what wasn't touched and can override.
For domains that exist but are thin (1 entry), still write new files into them, but never runtime edit an existing file in bootstrap mode. Edits are for the refresh phase (see Phase 2), not bootstrap.
Step 3 — Enrich the seed (factual spine)
Run these in parallel — they give you the factual scaffolding (industry, headcount, tech stack, funding) every downstream synthesis step will cite. All three key on the domain, so there is no id-resolution step:
DOMAIN=acme.com
# Parallel enrichments — same domain, three different signal families
for pair in "aiArk:enrichCompany" "builtwith:getDomainSummary" "enrichCrm:getFunding"; do
slug="${pair%%:*}"; action="${pair##*:}"
cargo-ai orchestration action execute \
--action "$(jq -nc --arg i "$slug" --arg a "$action" \
'{kind:"connector",integrationSlug:$i,actionSlug:$a}')" \
--data "{\"domain\":\"$DOMAIN\"}" \
--wait-until-finished > /tmp/enrich-$action.json &
done
wait
Total: ~1.01 credits (aiArk.enrichCompany 0.01, builtwith.getDomainSummary free, enrichCrm.getFunding 1). Drop the funding call and the whole spine costs a hundredth of a credit.
If every call comes back empty, fall back to website scraping only (Step 4) — note in every written file's ## Source section that firmographics were unavailable.
Step 4 — Scrape public sources in parallel sub-agents
Spawn one sub-agent per source. Each returns a structured digest (key claims + source URL), never raw HTML. Suggested fan-out:
| Sub-agent | Source URLs | Lands in |
|---|---|---|
| Website core | https://<domain>, /about, /product, /pricing, /customers |
global/positioning, global/narrative, global/mission, global/pricing, client/... |
| Careers | /careers, /jobs, LinkedIn jobs |
persona/..., signal/hiring-intent-... |
| Blog & launches | /blog, /changelog, /news |
insight/..., proof/... |
| News & funding | Google News, Crunchbase summary | signal/funding-..., proof/... |
| Reviews (depth=deep only) | G2, Capterra | objection/..., alternative/... |
| Communities (depth=deep only) | Reddit, HN search | objection/..., insight/... |
For each digest, require a source_url per claim. Skip anything you cannot source — a thin context beats a fabricated one.
Step 5 — Synthesize and write per domain
For each domain you intend to populate, read the template first so frontmatter (title, description) and section structure are valid. Missing title or description breaks the knowledge graph.
# Always read the template first
cargo-ai context runtime read --path global/_template.md
cargo-ai context runtime read --path persona/_template.md
cargo-ai context runtime read --path icp/_template.md
cargo-ai context runtime read --path client/_template.md
cargo-ai context runtime read --path proof/_template.md
cargo-ai context runtime read --path signal/_template.md
Then synthesize one markdown file per atomic concept directly — the agent running this recipe is already an LLM, so don't double-hop through cargo-ai orchestration action execute to call Anthropic / OpenAI. That pattern is for batch synthesis inside a workflow node graph (Play/Tool); here, the agent has the digest in context and can produce the file body itself.
For each domain, the agent should:
- Read the template (already done above) and the relevant digest from Step 4.
- Produce one complete markdown body per concept, including frontmatter (
title+description, both required), section structure from the template, and source URLs cited in## Sourceor## Day-to-day. - Write each file with
cargo-ai context runtime write. One concept per file — if you're tempted to write two## Personaheadings into one file, split into two files instead.
Example for persona/ (after the agent has drafted vp-engineering.md from the careers digest):
cargo-ai context runtime write \
--path persona/vp-engineering.md \
--content "$(cat <<'EOF'
---
title: VP of Engineering
description: Senior engineering leader at 50-500 person SaaS companies, owns platform reliability and developer productivity.
---
## Role
- Title: VP of Engineering
- Seniority: Executive
- Function: Engineering
- Reports to: CTO or CEO
## KPIs
- ...
## Source
- https://acme.com/careers/vp-engineering
EOF
)"
Step 6 — Verify and report
Re-run the graph to confirm the writes landed and surface any orphan cross-refs:
cargo-ai context graph get > /tmp/ctx-graph-after.json
# Node delta
echo "Before: $(jq '.nodes | length' /tmp/ctx-graph.json)"
echo "After: $(jq '.nodes | length' /tmp/ctx-graph-after.json)"
# Orphans (nodes referenced but not authored)
jq -r '.edges[] | select(.target.exists == false) | "\(.source.path) → \(.target.path)"' /tmp/ctx-graph-after.json
Report to the user:
- Files written, grouped by domain.
- Domains skipped (from Step 2).
- Orphan cross-refs (these usually mean the synthesis referenced a
persona/xthat wasn't actually written — either author the missing file or rewrite the reference).
Credit budget
| Step | Cost per call | Calls (depth=standard) | Subtotal |
|---|---|---|---|
| aiArk.enrichCompany | 0.01 | 1 | 0.01 |
| builtwith.getDomainSummary | 0 | 1 | 0 |
| enrichCrm.getFunding | 1 | 1 | 1 |
| Public-source scrapes (sub-agents) | 0 (agent LLM tokens, not Cargo credits) | 4–6 | 0 |
| Synthesis (agent native) | 0 (agent LLM tokens, not Cargo credits) | 6–8 | 0 |
| context runtime write | 0 | 15–30 files | 0 |
| Total (standard) | ~1 Cargo credit | ||
| Total (deep) adds review-site + community sub-agents | ~1 Cargo credit |
Bootstrap is one-shot per workspace. Re-running is a no-op for already-seeded domains thanks to Step 2's skip-list.
Action shape
{"kind":"connector","integrationSlug":"<slug>","actionSlug":"<slug>"}. No connectorUuid in config.
Output deliverable
A summary the agent presents to the user:
Context repo bootstrapped from acme.com:
Written (24 files):
global/ 3 files (positioning, narrative, pricing)
icp/ 2 files (mid-market-saas, enterprise-fintech)
persona/ 4 files (vp-eng, head-of-data, cto, vp-product)
client/ 5 files (3 enterprise, 2 mid-market)
proof/ 7 files (4 metrics, 3 quotes)
signal/ 3 files (hiring-intent-data-eng, series-c-funding, snowflake-adoption)
Skipped (already had content):
alternative/, objection/, insight/
Orphan refs: none.
Next steps:
- Open a fresh agent session so the seeded files load clean.
- Refine from real sales calls — see Phase 2 of lifecycle.md.
What this recipe does NOT do
- No refinement from sales calls. That's Phase 2 of
lifecycle.md— deliberately human-in-the-loop. Auto-promoting call-derived claims into context produces plausible-sounding but shallow ICPs. - No
runtime editon existing files. Bootstrap is additive only. Edits belong to the refresh phase. - No invention. If a claim has no
source_url, drop it. Thin context is recoverable; fabricated context erodes trust in everything downstream. - No promotion past the repetition threshold. See authoring rules of thumb. Bootstrap claims come from public sources, which count as one source — note the URL in the file body, don't promote to a confident assertion.
When stuck — file a workspace report
If context runtime write fails repeatedly, the workspace has no context repo configured, or a template has changed shape and the writes no longer match, file via:
cargo-ai workspaceManagement report create \
--title "bootstrap-from-domain: <one-line summary>" \
--description "<exact command(s) tried, errorMessage, domain attempted, workspace.uuid>"