@cargo-ai/cdk
v1.0.86
Published
Cargo CDK — define every Cargo resource (connectors, models, plays, tools, agents, MCP servers, folders, files, workers, apps, sending domains, mailboxes) in code and deploy it declaratively: plan, apply, refresh, import and destroy over a repo of definit
Maintainers
Readme
Cargo CDK
Define your entire Cargo workspace in code — connectors, models, plays, tools, agents, MCP servers, context, segments, capacities, territories, folders, files, workers, apps, sending domains and mailboxes — and deploy it declaratively, the same way you'd manage cloud infrastructure with Pulumi or the AWS CDK. For a complete overview, see the Cargo CDK docs.
Requirements
- Node.js 22.x
- A Cargo API token (create in your workspace under Settings → API)
Installation
@cargo-ai/cdk gives you both the define* builders you import in your resource
files and the cargo-cdk command to deploy them:
npm install @cargo-ai/cdk
npx @cargo-ai/cdk --helpcargo-cdk is a project-local tool — run it through the scaffolded npm run
scripts or npx @cargo-ai/cdk, the way you'd use the AWS CDK or SST. Don't install
it globally: a global cargo-cdk on your PATH would clash with the AWS CDK. (If you
also use the AWS CDK inside the same project, always call this one as
npx @cargo-ai/cdk to disambiguate.)
Authenticate with the Cargo CLI (or set a token for CI):
npm install -g @cargo-ai/cli # the Cargo CLI — `cargo-ai login` + the `cargo-ai project` alias
cargo-ai login --oauth # browser sign-in
# …or, for CI / non-interactive (no CLI needed):
export CARGO_API_TOKEN=<your-api-token>Login writes ~/.config/cargo-ai/credentials.json, which cargo-cdk reads (along
with the CARGO_API_TOKEN / CARGO_WORKSPACE_UUID env vars). Everywhere below you
run the CDK one of two ways: npm run <script> for the everyday commands (a
scaffolded project wires up types / plan / deploy), or npx
@cargo-ai/cdk <command> for any command — both resolve the project-local
cargo-cdk bin. The cargo-ai project <command> alias works too.
Quick start
Scaffold a GTM repo — npx @cargo-ai/cdk runs the CDK with no global install:
npx @cargo-ai/cdk init my-workspace
# pick a cookbook in the wizard, or afterwards:
# npx @cargo-ai/cdk add cookbook/<name>
cd my-workspace && npm installinit scaffolds getcargohq/cargo-manifest:
the context, cadence and evals layers, with the CDK project in infra/. Every
CDK command runs from there — the repo's root package.json proxies plan with
--prefix infra for the same reason.
Or add a resource file yourself — importing a file is registration, there's no manifest to maintain:
// my-workspace/contacts.ts
import { defineConnector, defineModel, secret } from "@cargo-ai/cdk";
// `secret()` reads the value at deploy time and keeps it out of state and the
// content hash — use it for credentials. The config shape is per-integration
// (HubSpot takes a Private App token or an OAuth refresh token); run
// `npx @cargo-ai/cdk types` to have it type-checked (see Typed config below).
export const hubspot = defineConnector("hubspot", {
integration: "hubspot",
config: { method: "privateApp", accessToken: secret("HUBSPOT_API_KEY") },
});
// Pass the connector handle as `connector` — the model depends on it, so the
// CDK creates the connector first and injects its dataset uuid.
export const contacts = defineModel("contacts", {
connector: hubspot,
extractSlug: "fetchRecords",
});export HUBSPOT_API_KEY=... # secret() reads this at deploy time
npm run info # what the project declares — no API calls
npm run plan # offline diff — no API calls (runs `cargo-cdk plan`)
npm run deploy # create everything, record it in the deploy state(The scaffolded package.json wires npm run types / info / plan / deploy
to the project-local cargo-cdk. Or run any command directly with
npx @cargo-ai/cdk <command>.)
Re-running deploy only changes what changed; an unchanged workspace is a no-op.
Typed config
defineConnector and defineModel config are typed against your workspace's
actual integration schemas. Generate the types once:
npm run types # or: npx @cargo-ai/cdk typesThey land in .cargo-ai/. Add it to your tsconfig include with an explicit
glob — a bare .cargo-ai (a dot-dir) is ignored by TypeScript:
// tsconfig.json
"include": ["*.ts", ".cargo-ai/**/*.d.ts"]After that, config is checked against the integration's schema — e.g. HubSpot
is a discriminated union, so the editor completes method and requires the
matching credential, and secret() is accepted wherever a credential is
expected. A uuid-reference config field (e.g. an extractor's modelUuid /
relatedModelUuid) is typed as In<string>, so you can pass a resource
handle's uuid directly (modelUuid: accounts.uuid) to wire the dependency — no
cast — or a literal uuid string. Re-run npm run types after adding or changing
workspace integrations.
Typing is a bonus, never a gate: an integration you haven't synced (or a custom
one) falls back to a loose Record<string, unknown>, so deploy still works
without ever running npm run types.
Defining resources
Every Cargo resource has a define* builder that returns a handle. Wire
resources together by passing one handle to another — the dependency graph is
just your variable graph. Pass the handle directly (or xxRef("uuid") for a
resource you didn't define in code); no .uuid at the call site. Where a
reference takes per-call options, wrap it as { ref, …options }.
import { defineAgent, defineTool, defineWorkflow, sendEmail } from "@cargo-ai/cdk";
const flow = defineWorkflow("enrich", { input, output }, ({ input, ai }) => …);
export const enrich = defineTool("enrich", { workflow: flow, emojiSlug: "mag" });
export const sdr = defineAgent("sdr", {
connector: openai, // the LLM connector (or connectorRef("…"))
languageModel: "gpt-4o",
systemPrompt: "Qualify inbound leads.",
// everything the agent can call or read — one array, kind inferred per handle
uses: [
{ ref: contacts, readOnly: true }, // a data model (handle + options)
enrich, // a tool (bare handle)
hunter.actions.findEmail, // an action off a connector handle
sendEmail, // a native action (same binding a workflow body calls)
],
});Referencing handles inside a workflow body
A defineWorkflow body is parsed from its source and never executed, so it can't
read closure values at runtime. The loader closes that gap: it scans each body's
free variables and threads the ones bound in the file (imports, module consts)
through the SDK's imports option automatically. So a build-time handle can be used
bare inside a body — no boilerplate:
const accounts = defineModel("accounts", { … });
const flow = defineWorkflow(
"tam",
{ input, output },
({ input, model }) =>
// `accounts` is a module-level handle — the loader injects it for you.
model.upsert({ modelUuid: accounts.uuid, matchingColumnSlug: "domain", matchingValue: input.domain }),
);The handle's uuid is a deferred token, so the model is ordered first on deploy
and its real uuid is substituted in. Constants work the same way (inlined as
values). Two things to know: a handle's token can't be used inside a computed
expression (only as a direct config value — it isn't a string until deploy), and
a workflow defined in a file that's reached only via a transitive import
(not scanned as a top-level resource file) bypasses this pass — the SDK then
throws an error telling you to pass its explicit imports option in that file.
See Defining Resources for every builder (connectors, models, plays, tools, agents, MCP servers, context, segments, capacities, territories, folders, files, workers, apps, domains) and the slug rules.
Native object and unified models (kind: "native") and the context
repository (defineContext) have their own guides:
Object models,
Unification, and
Context.
Commands
In a scaffolded project, the everyday commands are npm run scripts:
npm run types # generate per-workspace types for typed config
npm run info # offline: what the project declares, and any diagnostics
npm run plan # diff the code against this project's deploy state
npm run deploy # create/update resources, record them in the stateTwo of those answer different questions and it is worth knowing which is which.
plan answers what will deploy do — its unit is the change, and it exits
non-zero so CI can gate on it. info answers what is here — its unit is the
resource, including the file that declared it, and it never fails. Reach for
plan before deploying and info when a resource is not behaving the way its
file reads.
Running cargo-cdk with no arguments does whichever of those makes sense:
info inside a project, init outside one.
deploy is the only place the CDK spends money: a + create domain:… line
registers a domain against workspace credits and isn't refundable (and destroy
cancels it). Read the plan before confirming.
Run any command — including the ones without a script — with npx @cargo-ai/cdk:
npx @cargo-ai/cdk init # ask what to scaffold (a directory, and the repo or a cookbook)
npx @cargo-ai/cdk init <directory> # scaffold the Manifest repo
npx @cargo-ai/cdk info # the resolved project: every resource, its file, its state, diagnostics
npx @cargo-ai/cdk deploy --prune # also delete resources removed from code
npx @cargo-ai/cdk deploy --refresh # re-read live resources, re-apply out-of-band changes
npx @cargo-ai/cdk refresh # read-only: report resources that drifted from code
npx @cargo-ai/cdk import <id> <uuid> # bind an existing live resource into state
npx @cargo-ai/cdk rollback # restore the deploy state from the pre-deploy snapshot
npx @cargo-ai/cdk destroy --target <id> # tear down one resource
npx @cargo-ai/cdk destroy --all # tear down everything in stateEvery command also works as cargo-ai project <command> if you have the global
Cargo CLI installed — same engine, same login. cargo-ai cdk <command> is kept
as an alias of that group, so anything already written down still runs.
Common flags: --dir <path>, --yes (skip the prompt — required in CI),
--json, and --force (steal a stale state lock). See
Deploying.
Cookbooks
Cookbooks are worked CDK examples — a TAM pipeline, account scoring, CRM
enrichment — published as folders in
getcargohq/gtm-skills. add
installs one into a project you already have, as a sibling of what is there:
npx @cargo-ai/cdk cookbook list # every cookbook
npx @cargo-ai/cdk cookbook search scoring # by name, job, or the resources it declares
npx @cargo-ai/cdk cookbook view tam-building # what it deploys, what it will ask you for, how to adapt it
npx @cargo-ai/cdk add cookbook/tam-building # copy it inCopying is all add does. What follows — reconciling the example with what the
project already declares, adapting it to the real CRM and fields, the inputs it
wants looked up before any of them are asked — is the cookbook's own procedure,
and add prints it as a checklist (or hands it to a coding agent) rather than
ending on deploy.
A cookbook's infra/ lands under infra/<name>/, its scripts/ under
scripts/<name>/, and everything else — its SKILL.md, references/,
evals/ — under .claude/skills/<name>/, beside the skills the repo already
ships, and again under .agents/skills/<name>/:
infra/crm-enrichment/index.ts
scripts/crm-enrichment/seed.ts
.claude/skills/crm-enrichment/SKILL.md
.claude/skills/crm-enrichment/references/run.md
.claude/skills/crm-enrichment/evals/acceptance.md
.agents/skills/crm-enrichment/SKILL.md
.agents/skills/crm-enrichment/references/run.md
.agents/skills/crm-enrichment/evals/acceptance.mdThe skill half is written twice because no one directory is read by every
agent: Claude Code discovers project skills only under .claude/skills/, while
Cursor, Codex and Gemini read .agents/skills/. They are copies rather than
symlinks, which git does not carry reliably across a Windows checkout — so if
you edit a cookbook's procedure, edit both. The infra/ and scripts/ halves
are written once: resources are loaded by path rather than discovered, and a
second copy would register every resource twice; scripts live at the repo root
so the loader never imports them.
Each piece stays namespaced by cookbook, so two cookbooks that each bring a CRM
connector stay out of each other's way, and the references stay next to the
procedure that cites them. The loader scans every .ts under the project root,
so infra/<name>/ is discovered like any other directory.
add never touches your package.json or tsconfig.json, reports every file
that already exists and skips it (--overwrite replaces instead), runs check
afterwards so a duplicate slug surfaces immediately, and never deploys. Use it
rather than init --force, which replaces the project shell while the deploy
state survives — leaving the next plan diffing a live workspace against code
nobody wrote.
Handing off to a coding agent
When a person runs init or add and a coding agent (Claude Code, Cursor,
Codex, Gemini CLI) is on PATH, the last question is whether to open it on the
new project:
How would you like to continue?
❯ Open Claude Code build it with the cargo-cdk skill loaded
Exit print the next stepsIt opens in the project directory with a brief describing what was just
created: which commands this project has, that the cargo-cdk skill is the
authority on the define* builders, and that deploy spends credits and must
not be run unasked. After add, the brief points at the cookbook's own
.claude/skills/<name>/SKILL.md and orders the work left over it — start at
Adapt, not at the copy step that just ran; look these inputs up, ask for those
— rather than restating a procedure the cookbook already ships.
Running from a coding agent
Every command that could ask a question refuses to when there is no one to
answer: no TTY, CI=true, or a coding-agent harness (Claude Code, Cursor,
Codex, …). Instead of hanging on a prompt it exits 2 and prints the command to
re-run with the missing argument supplied. Passing the argument — as every
scripted invocation already does — is always the non-interactive path.
State & drift
A project's deploy state is the link from your code to the resources Cargo created. It lives in your workspace, and the repo commits a pointer to it:
{ "stateUuid": "8f2c…" }init creates the state and writes that cargo.state.json, so the pointer is in
the scaffold commit. Commit it — without the uuid a fresh checkout cannot
find its state, and a deploy will not quietly make a replacement (that would
orphan everything the old one tracks). The state records only uuids, hashes and
outputs — never secret values.
One state per repo, at most 10 live states per workspace:
npx @cargo-ai/cdk state list # every state here, and which one this project is on
npx @cargo-ai/cdk state create # create one for this project and write the pointer
npx @cargo-ai/cdk state bind <uuid> # repoint at an existing one
npx @cargo-ai/cdk state remove <uuid> # drop a state (does not delete live resources)A project scaffolded before states moved to the workspace has the resource map
itself in cargo.state.json, and keeps deploying against that file for as long
as you leave it there. state create is what moves it: the map goes onto a new
state, the file becomes a pointer, and you commit it. Nothing migrates a project
behind your back — a teammate who deploys before pulling that pointer would
otherwise be deploying from a state that no longer exists.
Git-ignore the generated types and the working files the CDK writes alongside it
(but commit cargo.state.json):
.cargo-ai/
cargo.state.lock
cargo.state.bak.json
cargo.state.cache.json
cargo.state.audit.jsonlnpx @cargo-ai/cdk refresh reports resources changed or deleted outside the CDK
(e.g. edited in the Cargo UI); npx @cargo-ai/cdk deploy --refresh re-applies your
code over them. See State & Drift.
Documentation
Full documentation: docs.getcargo.ai/cdk.
