# 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](https://docs.getcargo.ai/get-started/project-layout).

## 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:

```bash
npm install @cargo-ai/cdk
npx @cargo-ai/cdk --help
```

`cargo-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):

```bash
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:

```bash
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 install
```

`init` scaffolds [`getcargohq/cargo-manifest`](https://github.com/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:

```ts
// 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",
});
```

```bash
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:

```bash
npm run types          # or: npx @cargo-ai/cdk types
```

They 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:

```jsonc
// 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 }`.

```ts
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:

```ts
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](https://docs.getcargo.ai/connectors/overview) 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](https://docs.getcargo.ai/models/object-models),
[Unification](https://docs.getcargo.ai/models/unification), and
[Context](https://docs.getcargo.ai/context/overview).

## Commands

In a scaffolded project, the everyday commands are `npm run` scripts:

```bash
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 state
```

Two 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`:

```bash
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 state
```

Every 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](https://docs.getcargo.ai/deploy/deploying).

## Cookbooks

Cookbooks are worked CDK examples — a TAM pipeline, account scoring, CRM
enrichment — published as folders in
[`getcargohq/gtm-skills`](https://github.com/getcargohq/gtm-skills). `add`
installs one **into a project you already have**, as a sibling of what is there:

```bash
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 in
```

Copying 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>/`:

```text
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.md
```

The 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:

```text
How would you like to continue?
❯ Open Claude Code   build it with the cargo-cdk skill loaded
  Exit               print the next steps
```

It 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:

```json
{ "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:

```bash
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`):

```gitignore
.cargo-ai/
cargo.state.lock
cargo.state.bak.json
cargo.state.cache.json
cargo.state.audit.jsonl
```

`npx @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](https://docs.getcargo.ai/deploy/state-and-drift).

## Documentation

Full documentation: [docs.getcargo.ai/cdk](https://docs.getcargo.ai/get-started/project-layout).
