* Playbooks for parallel and exa, and a 20x price error in the stage map Section 11 claims every credits-based provider in the catalog has a playbook. Nine do not: brightData, builtwith, datachimp, exa, openRouter, parallel, proxycurl, sillage and x. This adds the two that matter most for research and leaves the claim to be squared with the other seven separately. Both are worth having because both beat what the recipes currently reach for. parallel.extract reads a known URL for 0.025, half of firecrawl.scrape. parallel.createTask is the only action in the catalog that fills a caller-supplied output schema, and at processor lite it is 0.125 against linkup.instruct at 1. exa.search is the only rung with a document-type category filter and publication-date bounds, which is what makes it worth 0.175 fixed plus 0.025 an item over a 0.05 flat search. Every action, input and price here was read from the live integration catalog rather than from documentation, per the lesson this pack keeps relearning: a capability claim needs the implementation, not the docs index. That is also how the error surfaced. references/stage-action-map.md priced serper.search at 1. It is 0.05, and provider-playbooks/serper.md had it right throughout, so the map was steering agents away from the cheapest search rung by a factor of twenty. alternatives.md carried the same wrong number for serper.searchPlaces. Both corrected, and the correction is stated in the table rather than made silently. The Web research table is rebuilt around the actual decision: known URL means parallel.extract, plain keyword means serper, document-type or date filter means exa, structured output means parallel.createTask at lite. The createTask ladder gets its own table and a warning. processor is required with no default, and the top rung is 60 credits per record, which is more than a full contact waterfall. Co-Authored-By: Auree Aubert <16732193+aureeaubert@users.noreply.github.com> Co-Authored-By: replicas-connector[bot] <replicas-connector[bot]@users.noreply.github.com> * Ban datachimp in code, and close three of the playbook gaps Two things, both about the same claim: section 11 said every credits-based provider has a playbook, and nine did not. Datachimp is removed everywhere and cannot come back. Aurelien's instruction, 2026-08-15. It appeared in the generated catalog and in two lines of cargo-storage prose. Deleting those would have lasted until the next --refresh, so the removal is enforced in code instead: a DENIED_INTEGRATIONS set filters it out of the catalog on both write and read, and skills-lint fails on the string anywhere in the repo, which is what catches the hand-written half no generator touches. It was also an integration with zero callable actions, so nothing was lost. Three playbooks added, each earning its place on price rather than on coverage. builtwith's getDomainSummary is FREE and answers most stack questions, so the playbook's whole argument is that it runs in front of enrichDomain (1); the current recipes reach straight for a paid rung. sillage's searchLeads is also free and is the only free signal rung in the catalog, which makes it first on any signal question by definition. x is fourteen actions at 0.02, the cheapest signal surface there is, and the playbook spends most of its length on why that is dangerous rather than on how to use it. Three are deliberately not written and the claim now names them instead of overstating. openRouter exposes a model lister and no credits-based actions, so there is nothing to document. proxycurl is excluded on instruction. brightData is excluded because its entire action set is consumer social-platform scraping (Instagram, TikTok, Facebook), which acceptable-use already refuses for person targeting: documenting it as a GTM provider would contradict the gate two files away. Every action, input and price read from the live integration catalog. Co-Authored-By: Auree Aubert <16732193+aureeaubert@users.noreply.github.com> Co-Authored-By: replicas-connector[bot] <replicas-connector[bot]@users.noreply.github.com> * x: a cost table a validator can read, not a grouping a human can The action list was grouped by input shape, which reads well and parses to nothing. Every other playbook here carries `| action | cost | inputs | use for |` and the downstream validator in getcargohq/gtm-skills asserts a skill's quoted price against that column, so a skill calling any x action would have failed with "not in that provider's cost table" and sent someone hunting for a missing price rather than a missing table shape. The four engagement-list actions keep their warning in the Use for column instead of only in the anti-patterns, since that column is what a reader scans. Co-Authored-By: Auree Aubert <16732193+aureeaubert@users.noreply.github.com> Co-Authored-By: replicas-connector[bot] <replicas-connector[bot]@users.noreply.github.com> * Install the whole bundle by default, and stop hand-counting installs skills.sh counts per skill, and one `skills add` sends a single event carrying every skill it installed. So the command in the README decides how many counters a real install moves. `--all` is now the documented form: it is shorthand for `--skill '*' --agent '*' -y` and behaves the same in a terminal, in CI, and inside an agent, where a bare add would drop into an interactive picker. Two stale hand-maintained facts removed rather than corrected, which is the same call this repo made about "sixteen skills" in the CLI help. The install total read "installed 54,885 times". The live figure is 58,245 across the seventeen skills. A number in prose that only moves when somebody remembers to move it will be wrong again next week, and this one is a public claim about traction, which is the worst kind to be casually wrong about. The gtm-skills section listed twelve skills by name. That repo has grown past twelve, so the list now points at its README instead: a hand-maintained copy of someone else's list is a copy that goes stale. Co-Authored-By: Auree Aubert <16732193+aureeaubert@users.noreply.github.com> Co-Authored-By: replicas-connector[bot] <replicas-connector[bot]@users.noreply.github.com> --------- Co-authored-by: replicas-connector[bot] <replicas-connector[bot]@users.noreply.github.com> Co-authored-by: Auree Aubert <16732193+aureeaubert@users.noreply.github.com>
6.7 KiB
Ingest models — get the webhook URL and POST records
Some models are fed by pushing records to Cargo instead of Cargo pulling them.
Their extractor has mode.kind === "ingest" — the canonical one is the http
integration's listenHook ("Listen webhook"), but the same mechanism backs
storeleads.listenList, rb2b.listenProfiles, albacross.listenWebsiteVisits,
and others.
The app shows a Webhook URL on the model's settings screen. There is no CLI command and no API field that returns it — the app builds the string client-side. You can build the exact same string from data the CLI already exposes.
The URL
<baseUrl>/v1/models/<model-uuid>/records/ingest?token=<api-token>
<baseUrl>—cargo-ai whoami→.baseUrl(e.g.https://api.getcargo.io). Note the path is/v1/models/..., not/v1/storage/models/....<model-uuid>— the ingest model's UUID.<api-token>— any workspace API token. The token may carry zero permissions; this route is explicitly allowed for permission-less tokens so the URL can be handed to a third-party system safely. The app auto-creates one namedQuick accessfor exactly this.
token can also be sent as an Authorization: Basic <token> header instead of a
query param — preferable when the receiving system supports custom headers, since
a query param lands in logs.
Derive it
# 1. Find the model and confirm it is an ingest model
cargo-ai storage model get <model-uuid> | jq '{uuid, slug, extractorSlug, kind, connectorUuid}'
# 2. Confirm the extractor's mode is "ingest" (and NOT autoIngest — see below)
cargo-ai connection integration get http | jq -c '.integration.extractors.listenHook.mode'
# → {"kind":"ingest"}
# 3. Pick or create a token (the raw value is on the list response)
cargo-ai workspaceManagement token list | jq -r '.tokens[0].token'
cargo-ai workspaceManagement token create --name "Webhook — <model-slug>" | jq -r '.token.token'
One-liner that assembles it:
MODEL_UUID=<model-uuid>
BASE=$(cargo-ai whoami | jq -r '.baseUrl')
TOKEN=$(cargo-ai workspaceManagement token list | jq -r '.tokens[0].token')
echo "$BASE/v1/models/$MODEL_UUID/records/ingest?token=$TOKEN"
Token values are secrets. Print the URL for the user to copy; don't write it into a file, a commit, or a report.
Skip models where Cargo owns the hook
Some ingest extractors set autoIngest: true — Cargo registers the webhook with
the provider itself during setup (calendly, smartlead, instantlyV2, heyReach,
and cargo's own signal extractors). The app hides the URL for
those, and handing it out is wrong: the provider is already pointed at it.
Check before showing anything:
cargo-ai connection integration get <integration-slug> \
| jq -c '.integration.extractors["<extractor-slug>"].mode'
# {"kind":"ingest"} → manual: show the URL
# {"kind":"ingest","autoIngest":true} → Cargo owns it: don't show the URL
# anything else (fetch/…) → not an ingest model at all
List every ingest extractor an integration has:
cargo-ai connection integration get <integration-slug> \
| jq -c '.integration.extractors | to_entries
| map(select(.value.mode.kind=="ingest")) | map({(.key): .value.mode})'
# calendly → [{"fetchEvents":{"kind":"ingest","autoIngest":true}}]
POST records
The body is either one flat object or an array of flat objects — each object becomes one record, and its keys become columns. Max 100 records per request.
# one record
curl -X POST "$BASE/v1/models/$MODEL_UUID/records/ingest?token=$TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com","company":"example.com"}'
# many records
curl -X POST "$BASE/v1/models/$MODEL_UUID/records/ingest?token=$TOKEN" \
-H "Content-Type: application/json" \
-d '[{"email":"ada@example.com"},{"email":"grace@example.com"}]'
# token as a header instead of a query param
curl -X POST "$BASE/v1/models/$MODEL_UUID/records/ingest" \
-H "Content-Type: application/json" \
-H "Authorization: Basic $TOKEN" \
-d '{"email":"ada@example.com"}'
# all → 200 {"message":"OK"}
The endpoint is insert-only. Do not send an envelope like
{"kind":"insert","records":[…]} — there is no unwrapping, so you get one useless
row with a kind column and a records column holding the stringified array.
The {kind: insert|update|remove, records: […]} shape belongs to the extractor's
internal contract, not to this HTTP body; update and remove are not reachable
through the webhook.
For http.listenHook, the model's id column is _ingest_id (a UUID generated
server-side — never send it; the extractor rejects inserts that carry one) and
its title column is _emitted_at.
Then confirm the rows landed (storage queries are free):
cargo-ai storage query execute "SELECT * FROM <dataset-slug>.<model-slug> LIMIT 10"
Create an ingest model from scratch
model create has no --connector-uuid — the API infers the connector from the
dataset. Every connector automatically owns exactly one kind: "connector"
dataset, so the flow is: create the connector, find its dataset, create the model
in it.
# 1. Connector (slug must be snake_case: /^[a-z0-9]+(_[a-z0-9]+)*$/)
cargo-ai connection connector create \
--name "Inbound leads" --slug inbound_leads \
--integration-slug http --config '{}' | jq -r '.connector.uuid'
# 2. Its dataset — dataset list takes no --connector-uuid filter, so filter locally
cargo-ai storage dataset list \
| jq -c --arg c <connector-uuid> '.datasets[] | select(.connectorUuid==$c) | {uuid, slug}'
# 3. The model
cargo-ai storage model create \
--slug inbound_leads --name "Inbound Leads" \
--dataset-uuid <dataset-uuid> \
--extractor-slug listenHook --config '{}'
The response comes back with kind: "connector", idColumnSlug: "_ingest_id",
and titleColumnSlug: "_emitted_at". Columns are then created dynamically from
the keys of whatever you POST — you don't declare them up front. Query it as
<connector-slug>.<model-slug>.
Notes
- The endpoint answers webhook handshakes out of the box — Slack
url_verification, genericping, Microsoft?validationToken=, Salesforce SOAP ack, and Meta'shub.challengeonGET— so most providers validate without extra work. - Ingest models can't be refreshed or scheduled; data only arrives when something
POSTs.
model refresh/--scheduledon't apply. - A legacy alias
POST /v1/workflows/<uuid>/hookstill resolves to the same insert (the uuid being the model uuid). Prefer the/v1/models/...form. - Because the URL is assembled client-side, it is derived, not returned — if
a future CLI release adds a field or a
get-webhook-urlcommand, prefer that.