getcargohq/cargo-skills包含需要注意的行为
SKILL DETAIL
cargo-cdk
getcargohq/cargo-skills/cargo-cdk
cargo-cdk 技能允许您将整个 Cargo 工作区作为代码进行管理。您可以使用 TypeScript 中的 define* 构建器声明连接器、模型、剧本、工具、代理、MCP 服务器、分段、上下文、文件夹、文件、工作线程和应用,然后使用 cargo-ai cdk 命令(init → types → plan → deploy)将这些声明与实时基础设施进行协调,类似于使用 Pulumi 或 AWS CDK。 该技能适用于需要可重复、版本控制且可跨环境(如开发和生产)部署的场景。它提供了完整的生命周期管理,包括计划(离线差异)、部署(按依赖顺序创建/更新资源)、销毁(拆除资源)、刷新(漂移检测)、导入(采用现有资源)和回滚。它还支持生成类型化配置,以确保您的定义与工作区的实际集成模式匹配。
安装量 · 135查看来源
Installation
npx skills add https://github.com/getcargohq/cargo-skills --skill cargo-cdk
技能文件
SKILL.md
最近同步 · 2026年8月29日
guides/authoring-resources.md›
# Authoring resources
Every Cargo resource has a `define*` builder imported from `@cargo-ai/cdk`. Each
returns a **handle**; you wire resources together by passing one handle into
another builder. **Importing a `.ts` file is registration** — each `define*` call
registers itself as a side effect, so there is no manifest to maintain. The loader
imports every `.ts` under the project directory (skipping worker/app bundle
sub-directories, which have their own `package.json`).
For the exact spec fields and outputs of each builder, see
[`../references/resources.md`](../references/resources.md).
## The handle / ref model (the one thing to internalize)
Pass the **handle** a builder returned — never `handle.uuid`:
```ts
export const contacts = defineModel("contacts", {
dataset: hubspot, // ← the connector handle, not hubspot.datasetUuid
});
```
The CDK reads the dependency (connector → model), deploys the connector first, and
injects its real dataset uuid at deploy time. Your variable graph *is* the
dependency graph.
For a resource you did **not** define in code (an existing connector, folder,
etc.), use a `xxRef("uuid")` helper — it produces the same handle shape from a
literal uuid:
```ts
import { defineModel, connectorRef } from "@cargo-ai/cdk";
export const leads = defineModel("leads", {
dataset: connectorRef("6f0c…"), // an already-authenticated connector, by uuid
});
```
Ref helpers: `connectorRef`, `datasetRef`, `modelRef`, `folderRef`, `playRef`,
`memberRef` (from `@cargo-ai/cdk`) and `toolRef`, `agentRef` (re-exported from the
workflow SDK). Each is branded by kind, so a model can't be passed where a
connector is expected.
**Per-call options:** where a reference takes options, wrap it as
`{ ref, …options }`:
```ts
models: [{ ref: contacts, readOnly: true }], // handle + options
subAgents: [{ ref: enricher, waitUntilFinished: true }],
tools: [enrich], // bare handle when no options
```
## Secrets — `secret()` + `env()`
Wire credentials with `secret("ENV_VAR")`. The value is read from the environment
**at deploy time**, kept out of the content hash and out of `cargo.state.json`, so
rotating the token never reads as drift:
```ts
import { defineConnector, secret } from "@cargo-ai/cdk";
export const hubspot = defineConnector("hubspot", {
integration: "hubspot",
config: { method: "privateApp", accessToken: secret("HUBSPOT_API_KEY") },
});
```
`secret()` is only accepted where a credential/encryption field is expected. Use
`env("NAME")` for non-secret config values that come from the environment. Export
the variables before deploying — a missing one fails deploy with an unresolved
`${NAME}` placeholder rather than sending a literal to the API.
## The builder catalog
Data & schema:
```ts
// Connector — a data source or LLM provider. Creating a data connector
// auto-creates a dataset that models source from. OAuth connectors that can't be
// declared in code use `adopt: true` to link the existing authenticated instance.
export const hubspot = defineConnector("hubspot", {
integration: "hubspot",
config: { method: "privateApp", accessToken: secret("HUBSPOT_API_KEY") },
});
export const openai = defineConnector("open_ai", { integration: "openAi", adopt: true });
// Model — a table sourced from a connector's dataset by an extractor.
export const contacts = defineModel("contacts", {
dataset: hubspot, // connector handle → its dataset is injected
extractSlug: "fetchRecords",
config: { objectType: "contacts", columnSelectionMode: "all" },
folder: modelsFolder,
schedule: { type: "cron", cron: "0 * * * *" },
});
```
Automation:
```ts
// Tool — backed by a workflow. defineWorkflow compiles the logic; defineTool
// creates the tool and deploys that workflow as its release.
const enrichFlow = defineWorkflow(
"enrich-contact",
{ input: z.object({ email: z.string() }), output: z.object({ company: z.string() }), uses: { enricher } },
({ input, uses }) => ({ company: uses.enricher({ prompt: `Company for ${input.email}?` }) }),
);
export const enrich = defineTool("enrich", { workflow: enrichFlow, emojiSlug: "mag" });
// Play — runs a per-row workflow as a data model's rows change.
export const onboarding = definePlay("onboarding", {
model: contacts,
workflow: onboardRow,
changeKinds: ["added", "updated"],
runCreationRule: "always",
schedule: { type: "watch" },
});
// Agent — references models, tools, sub-agents, connector actions, and the LLM
// connector, each by handle.
export const sdr = defineAgent("sdr", {
connector: openai,
languageModel: "gpt-4o",
systemPrompt: "Qualify inbound leads.",
models: [{ ref: contacts, readOnly: true }],
tools: [enrich],
subAgents: [{ ref: enricher, waitUntilFinished: true }],
connectorActions: [{ integration: "hunter", actionSlug: "emailFinder" }],
folder: agentsFolder,
});
// MCP server — bundles tools, agents, and models behind one MCP endpoint.
export const crm = defineMcpServer("crm", { tools: [enrich], agents: [sdr], models: [{ ref: contacts }] });
```
Organization & knowledge:
```ts
// Folder — per-kind (a "model" folder and an "agent" folder are separate).
export const modelsFolder = defineFolder("crm-models", { kind: "model", name: "CRM" });
// RECOMMENDED: route every CDK-managed resource into a dedicated, clearly
// labelled folder (via each builder's `folder:`), so a human in the UI sees at a
// glance that these resources are owned by code and shouldn't be hand-edited —
// manual UI changes read back as drift on the next `plan`. Because folders are
// per-kind, give each kind its own but share ONE short, recognizable prefix, e.g.
// `🔒 CDK` (or `🔒 CDK Models`, `🔒 CDK Agents`). Keep names short — long labels
// truncate in the folder tree; the lock emoji is the "don't touch" cue.
export const cdkModels = defineFolder("cdk-models", { kind: "model", name: "🔒 CDK Models" });
export const cdkAgents = defineFolder("cdk-agents", { kind: "agent", name: "🔒 CDK Agents" });
// File — content uploaded from a local path (hashed at define time, so edits show as drift).
export const playbook = defineFile("playbook", {
path: new URL("./playbook.md", import.meta.url).pathname,
name: "SDR Playbook",
});
// Context — the workspace's git-backed GTM knowledge base as code. Singleton;
// additive (files added in the UI are left in place).
export const context = defineContext({ dir: "context" });
```
Revenue org & segmentation:
```ts
// Segment — a saved view over a model (filter is required; model is immutable).
export const hotLeads = defineSegment("hot-leads", { model: contacts, filter: { /* … */ } });
// Capacity / Territory — revenue-org planning over members and a model.
export const capacity = defineCapacity("ae-capacity", { model: contacts, /* … */ });
export const territory = defineTerritory("west", { model: contacts, members: [memberRef("…")] });
```
Hosting (async builds — see [`deploy-and-state.md`](deploy-and-state.md)):
```ts
// Worker — the hosted worker slot. `path` points at a BUILT bundle dir
// (index.js + manifest.json + package.json + package-lock.json). Author the
// runtime code with createWorker from @cargo-ai/worker-sdk and build to index.js.
export const webhook = defineWorker("webhook", {
path: new URL("./webhook", import.meta.url).pathname,
description: "Receives inbound lead webhooks.",
});
// App — a hosted Vite SPA. `path` points at the app package root.
export const dashboard = defineApp("dashboard", {
path: new URL("./dashboard", import.meta.url).pathname,
});
```
Observability:
```ts
// Alert — a scheduled threshold check; fires actions as runs on breach. The
// scope wires the watched resource by handle, and scope + threshold are a
// matched pair (TS narrows the metric menu to the scope's kind).
export const syncErrors = defineAlert("crm-sync-errors", {
schedule: { type: "cron", cron: "*/30 * * * *" },
scope: { kind: "runs", workflow: onboarding }, // the definePlay handle
threshold: { metric: "errorRate", operator: "gte", value: 10 },
actions: [
// Bare { ref, config } for an agent; config is templated against the firing
// context. Typed connector/tool actions use the alertConnectorAction /
// alertToolAction helpers (config checked against the action's input schema).
{ ref: sdr, config: { message: "CRM sync error rate at {{event.value}}% — {{alert.url}}" } },
],
});
```
`defineAlert` is the declarative front for the observability domain — the full
scope/threshold matrix, metric units, and firing semantics live in
[`../../cargo-observability/SKILL.md`](../../cargo-observability/SKILL.md).
The full `full` template wires all of the above together end-to-end — see
[`../references/examples/full-workspace.md`](../references/examples/full-workspace.md).
## Workflow bodies (`defineWorkflow`) — parsed, not executed
`defineTool` and `definePlay` take a `workflow:` built with `defineWorkflow`. The
body callback is **parsed, not executed** — the SDK reads the function's source
and lowers it into the engine's node graph. So the body must be a supported JS
subset (no `await`, `throw`, `try/catch`, closures, or destructuring).
```ts
defineWorkflow(
"onboard-contact",
{
input: z.object({ email: z.string() }),
output: z.object({ welcomed: z.boolean(), message: z.string() }),
uses: { enrich }, // reference tools/agents by handle here (or toolRef/agentRef)
},
({ input, uses, ai, integrations, native }) => {
const enriched = uses.enrich({ email: input.email }); // typed to the tool's I/O
const message = ai(`Welcome ${input.email} at ${enriched.company}.`);
return { welcomed: true, message };
},
);
```
- **`uses.<key>(input)`** calls a referenced tool/agent; it's typed to that
resource's input and returns a `Ref` you can dot-access (`enriched.company`).
- **`integrations.<slug>.<action>({…})`** calls a connector action. The
`integrations` registry is **empty until you run `cargo-ai cdk types`** (see
[`typed-config.md`](typed-config.md)); `native.*` works without a sync.
- Control flow lowers idiomatically: `if/else` → branch, `else if` → switch,
`for (const x of xs)` → group. `ai("…")` inline-completes; `js(({nodes}) => …)`
is the escape hatch for logic outside the supported subset. Flow helpers:
`balance`, `split`, `humanReview`, `memory`.
For the full workflow-authoring surface (per-call retry/fallback, the supported-JS
table, toolchain requirements), that lives in the `@cargo-ai/workflow-sdk` docs —
`defineWorkflow` is re-exported from `@cargo-ai/cdk`, so you import it from the one
package.
guides/deploy-and-state.md›
# Deploy & state
The deploy engine compiles your `define*` graph, diffs it against
`cargo.state.json`, and reconciles the difference to live Cargo infrastructure in
dependency order — persisting state after **each** resource so a mid-deploy crash
leaves a recoverable file.
For every flag, see [`../references/commands.md`](../references/commands.md).
## plan → deploy → destroy
```bash
# Offline: compile the graph and diff against cargo.state.json. No API calls.
cargo-ai cdk plan --dir my-workspace
# Create/update resources in dependency order; write cargo.state.json.
cargo-ai cdk deploy --dir my-workspace # prompts for confirmation
cargo-ai cdk deploy --dir my-workspace --yes # non-interactive (CI)
# Tear down.
cargo-ai cdk destroy --dir my-workspace --target model:contacts # one resource
cargo-ai cdk destroy --dir my-workspace --all # everything in state
```
Re-running `deploy` only changes what changed — an unchanged workspace is a no-op.
`deploy` is idempotent: for each resource it updates by uuid if state has one,
otherwise adopts a slug-addressable match (connector/model) that already exists,
otherwise creates.
## `cargo.state.json` — commit it
`deploy` writes `cargo.state.json`: the link from your code to the resources Cargo
created. It records only `{hash, uuid, outputs}` per resource — **never secret
values**.
**Commit it.** It is the *only* handle on a deployed **play** or **agent**, which
have no slug (unlike connectors and models, which self-heal by slug). Losing state
orphans those resources. If it happens, re-establish a link with `import` (below).
Git-ignore the generated types and the CDK's working files (but **not**
`cargo.state.json`). `cargo-ai cdk init` scaffolds this:
```gitignore
.cargo-ai/
cargo.state.lock
cargo.state.bak.json
cargo.state.audit.jsonl
```
The `cargo.state.lock` prevents two deploys racing; `--force` steals a stale lock.
**Workspace guard:** state records the workspace it was deployed to; the CDK
refuses to `deploy`/`destroy` when the state's workspace ≠ the currently selected
workspace, so you can't accidentally reconcile a dev definition into prod. Select
the right workspace at `login` (or with the workspace flag) before deploying.
## Prune — deleting resources removed from code
`deploy` does **not** delete a resource just because you removed it from code —
that would make a typo destructive. To also remove resources that are in state but
no longer in code:
```bash
cargo-ai cdk deploy --dir my-workspace --prune
```
Prune deletes in reverse dependency order (dependents before their dependencies).
Adopted resources (linked via `adopt: true` or `import`) are **released** from
state, not deleted.
## Drift — `refresh` and `deploy --refresh`
A resource can change outside the CDK (someone edits an agent in the Cargo UI).
The CDK captures a fingerprint of each resource at deploy and compares on refresh:
```bash
cargo-ai cdk refresh --dir my-workspace # read-only: report what drifted
cargo-ai cdk deploy --dir my-workspace --refresh # re-read live, re-apply your code over drift
```
`refresh` reports resources changed or deleted out-of-band; `deploy --refresh`
makes your code the source of truth again.
## Adopting existing resources — `import`
To bring an already-live resource under CDK management, bind it into state by
mapping its **code id** to its **live uuid**:
```bash
cargo-ai cdk import model:contacts 6f0c8e2a-… --dir my-workspace
```
The code id is `kind:slug` (e.g. `connector:hubspot`, `model:contacts`,
`agent:sdr`). After import, `deploy` updates that resource instead of creating a
duplicate. Slug-addressable kinds (connector, model) can also self-adopt on deploy
by matching slug; uuid-only kinds (play, agent, capacity, territory, segment) need
`import` to recover a lost link. See
[`../recipes/migrate-existing-workspace.md`](../recipes/migrate-existing-workspace.md).
## Recovery — `rollback`
`deploy` snapshots the pre-deploy state to `cargo.state.bak.json`. If a deploy went
wrong, restore the snapshot:
```bash
cargo-ai cdk rollback --dir my-workspace
```
This restores the state file — it does not undo live API changes already made;
follow with a corrected `deploy`.
## Async resources — workers & apps
Most resources create synchronously. **Workers and apps build server-side**: on
deploy the reconciler uploads the bundle, waits for the build, and promotes it —
so a deploy touching a worker/app takes longer. Author worker runtime code with
`createWorker` (`@cargo-ai/worker-sdk`) and build to `index.js` before deploying;
the CDK validates the bundle files (`index.js`, `manifest.json`, `package.json`,
`package-lock.json`) exist at define time. The live URL is exposed on the handle
(`webhook.url`, `dashboard.url`). For imperative one-off hosting operations, see
[`../../cargo-hosting/SKILL.md`](../../cargo-hosting/SKILL.md).
guides/typed-config.md›
# Typed config — `cargo-ai cdk types`
`defineConnector`/`defineModel` config and the `integrations.*` registry in
workflow bodies are typed against **your workspace's real integration schemas**.
Those types aren't bundled (they're workspace-specific) — you generate them:
```bash
cargo-ai cdk types --dir my-workspace
```
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` works
without ever running `cdk types`.
## What it generates
Everything lands in `.cargo-ai/` (git-ignored):
- **`.cargo-ai/cargo-types.d.ts`** — `declare module` augmentations:
- types `@cargo-ai/cdk`'s `defineConnector`/`defineModel` `config` against your
connector and extractor schemas. E.g. HubSpot's config becomes a discriminated
union — the editor completes `method` and requires the matching credential,
and `secret()` is accepted wherever a credential is expected.
- adds your workspace's integration slugs to the `Integrations` interface, so
`integrations.<slug>.<action>({ … })` in a workflow body typechecks against the
real action schema.
- **`.cargo-ai/cargo-register.ts`** — eager `registerIntegration` /
`registerNative` calls so those slugs are **callable at runtime** in workflow
bodies.
Re-run `cargo-ai cdk types` whenever your workspace's integrations change (added a
connector, changed an extractor).
## Wiring it into your project
`cdk init` sets this up; for a hand-rolled project, two steps:
1. **Add the glob to `tsconfig.json` `include`.** A bare `.cargo-ai` (a dot-dir) is
ignored by TypeScript — you must use an explicit glob:
```jsonc
// tsconfig.json
"include": ["**/*.ts", ".cargo-ai/**/*.d.ts"]
```
2. **Import the runtime registrations** at the top of workflow modules that use
`integrations.*`:
```ts
import "./.cargo-ai/cargo-register.js";
```
(`native.*` needs no registration — the platform's native actions are identical
in every workspace, so their types ship with the SDK. Tools and agents are not a
registry either — reference them by handle through `defineWorkflow`'s `uses`.)
## Symptoms that mean "run `cdk types`"
- `config` on a `defineConnector` isn't autocompleting / isn't rejecting a wrong
credential shape → types not generated (or `.cargo-ai/**/*.d.ts` not in
`include`).
- `integrations.myConnector` is `any` / not callable at runtime → missing the
`cargo-register` import, or types are stale after an integration change.
- A connector `deploy` fails with `Invalid configuration` → often the credential
wasn't wrapped in `secret()`; generating types surfaces the required shape at
author time. See [`../references/troubleshooting.md`](../references/troubleshooting.md).
recipes/add-connector-and-model.md›
# Recipe: add a connector and a model sourced from it
**Use when** the user wants a new data source plus a model built from it, managed
as code. The key move is wiring the model to the connector **by handle** — the CDK
deploys the connector first and injects its dataset uuid.
## 1. Define the connector
Creating a data connector auto-creates a **dataset** that models source from. Put
the credential behind `secret()`.
```ts
// connectors/hubspot.ts
import { defineConnector, secret } from "@cargo-ai/cdk";
export const hubspot = defineConnector("hubspot", {
integration: "hubspot",
config: { method: "privateApp", accessToken: secret("HUBSPOT_API_KEY") },
});
```
For an OAuth/key connector you authenticated in the UI and can't declare in code,
use `adopt: true` so the reconciler links the existing instance instead of creating
one:
```ts
export const openai = defineConnector("open_ai", { integration: "openAi", adopt: true });
```
## 2. Define the model, wired to the connector
Pass the **connector handle** as `dataset` — not `hubspot.datasetUuid`:
```ts
// models/contacts.ts
import { defineModel } from "@cargo-ai/cdk";
import { hubspot } from "../connectors/hubspot";
export const contacts = defineModel("contacts", {
dataset: hubspot, // ← handle: model depends on connector
extractSlug: "fetchRecords",
config: { objectType: "contacts", columnSelectionMode: "all" },
schedule: { type: "cron", cron: "0 * * * *" }, // refresh hourly
});
```
If the connector already exists (not defined in code), reference it by uuid:
`dataset: connectorRef("<connector-uuid>")` — or `datasetRef("<dataset-uuid>")` to
point at a specific dataset.
## 3. Type the config (optional but recommended)
```bash
cargo-ai cdk types # now `config` on both builders type-checks against HubSpot's schema
```
## 4. Deploy
```bash
export HUBSPOT_API_KEY=...
cargo-ai cdk plan # shows: create connector:hubspot, create model:contacts
cargo-ai cdk deploy
git add cargo.state.json && git commit -m "Add HubSpot connector + contacts model"
```
The plan orders the connector before the model automatically because the model's
`dataset` handle depends on it. Re-deploying after a config edit updates in place.
recipes/build-an-agent.md›
# Recipe: build an agent (model + tool + agent)
**Use when** the user wants an AI agent with a data model, a tool, and an LLM
connector — all as code. Everything wires by handle, so the CDK deploys in
dependency order.
## 1. The LLM connector
An agent needs a language-model provider connector. Adopt an existing one:
```ts
// connectors/openai.ts
import { defineConnector } from "@cargo-ai/cdk";
export const openai = defineConnector("open_ai", { integration: "openAi", adopt: true });
```
## 2. A tool, backed by a workflow
`defineWorkflow` compiles the logic (parsed, not executed); `defineTool` deploys it
as the tool's release. Reference other resources inside the body via `uses`.
```ts
// tools/enrich.ts
import { defineTool, defineWorkflow } from "@cargo-ai/cdk";
import { z } from "zod";
const enrichFlow = defineWorkflow(
"enrich-contact",
{
input: z.object({ email: z.string() }),
output: z.object({ company: z.string(), enriched: z.boolean() }),
},
({ input, integrations }) => {
const found = integrations.hunter.companyEnrichment({ domain: input.email });
return { company: found.name, enriched: true };
},
);
export const enrich = defineTool("enrich", {
workflow: enrichFlow,
description: "Enrich a contact with firmographic data.",
emojiSlug: "mag",
});
```
> `integrations.*` is typed and callable only after `cargo-ai cdk types` (it reads
> your workspace's integrations). `native.*` works without it. See
> [`../guides/typed-config.md`](../guides/typed-config.md).
## 3. The agent, referencing model + tool + connector
Pass each dependency as a handle — bare, or `{ ref, …options }` when it needs
options:
```ts
// agents/sdr.ts
import { defineAgent } from "@cargo-ai/cdk";
import { openai } from "../connectors/openai";
import { contacts } from "../models/contacts";
import { enrich } from "../tools/enrich";
export const sdr = defineAgent("sdr", {
connector: openai,
languageModel: "gpt-4o",
systemPrompt: "You qualify inbound leads and enrich missing contact info.",
maxSteps: 12,
capabilities: ["webSearch", "memory"],
models: [{ ref: contacts, readOnly: true }],
tools: [enrich],
triggers: [{ type: "cron", cron: "0 9 * * *", text: "Daily qualification" }],
evaluator: { rubric: "Did it correctly qualify the lead?", threshold: 0.8 },
});
```
Add a sub-agent with `subAgents: [{ ref: enricher, waitUntilFinished: true }]`, or a
raw connector action with
`connectorActions: [{ integration: "hunter", actionSlug: "emailFinder" }]`.
## 4. Deploy
```bash
cargo-ai cdk types # so integrations.* in the workflow body typecheck
cargo-ai cdk plan # orders: connector → tool (+ its workflow) → model → agent
cargo-ai cdk deploy
git add cargo.state.json && git commit -m "Add SDR agent"
```
Because `agent:sdr` has no slug, `cargo.state.json` is the **only** handle on it —
commit it, or the next deploy can't find it (recover with `cdk import agent:sdr <uuid>`).
recipes/deploy-from-ci.md›
# Recipe: deploy from CI
**Use when** the user wants `cargo-ai cdk deploy` to run non-interactively — on
push, on merge, or on a schedule — so the workspace stays in sync with the repo.
## Prerequisites
- **A workspace-scoped API token** (not OAuth) from **Settings → API**. Store it as
a CI secret (`CARGO_API_TOKEN`). Token values are shown once — capture at
creation. See [`../../cargo-workspace-management/SKILL.md`](../../cargo-workspace-management/SKILL.md)
for `token create`.
- **`cargo.state.json` committed** in the repo — CI diffs against it. Without it,
CI can't tell what already exists and may recreate resources.
- **Any `secret()` env vars** set as CI secrets too (e.g. `HUBSPOT_API_KEY`).
## The CI steps
```bash
# 1. Install the CLI (project already depends on @cargo-ai/cdk via package.json)
npm install -g @cargo-ai/cli@latest
npm ci
# 2. Authenticate non-interactively with the token (selects the token's workspace)
cargo-ai login --token "$CARGO_API_TOKEN"
# 3. Deploy — --yes is REQUIRED (no TTY to confirm at); --json for machine-readable output
cargo-ai cdk deploy --yes --json
```
## Critical CI rules
- **`--yes` is mandatory.** `deploy`/`destroy` prompt for confirmation and refuse
to run non-interactively without it.
- **The workspace guard still applies.** `cargo.state.json` records the workspace it
was deployed to; deploy refuses if the token's workspace ≠ the state's workspace.
Use one state file per workspace (e.g. a branch or directory per environment).
- **Commit the updated state back** if CI is the source of truth. A deploy that
creates resources writes new uuids into `cargo.state.json`; if CI doesn't commit
them, the next run won't know they exist. Either commit the file from the CI job,
or make deploys only ever run from a branch whose state is already current.
- **Preview safely** with `cargo-ai cdk plan --json` (offline, no API calls) on pull
requests, and gate `deploy` to the protected branch.
- **Prune deliberately.** Add `--prune` only when you want CI to delete resources
removed from code; leave it off to make deploys purely additive.
## A GitHub Actions sketch
```yaml
# .github/workflows/deploy.yml
- run: npm install -g @cargo-ai/cli@latest && npm ci
- run: cargo-ai login --token "$CARGO_API_TOKEN"
env:
CARGO_API_TOKEN: ${{ secrets.CARGO_API_TOKEN }}
- run: cargo-ai cdk deploy --yes --json
env:
HUBSPOT_API_KEY: ${{ secrets.HUBSPOT_API_KEY }}
```
recipes/migrate-existing-workspace.md›
# Recipe: bring an existing workspace under CDK management
**Use when** a workspace already has live resources (built in the UI or the
imperative CLI) and the user wants to manage them as code going forward — without
creating duplicates.
The tool is `cargo-ai cdk import <id> <uuid>`, which binds a resource's **code id**
(`kind:slug`) to its **live uuid** in `cargo.state.json`. After import, `deploy`
updates that resource in place instead of creating a new one.
## 1. Write the `define*` for the resource
Describe the existing resource in code as accurately as you can. Match the slug you
want to address it by:
```ts
// models/contacts.ts — describe the live model
export const contacts = defineModel("contacts", {
dataset: connectorRef("<connector-uuid>"),
extractSlug: "fetchRecords",
config: { objectType: "contacts", columnSelectionMode: "all" },
});
```
## 2. Find the live uuid
Use the matching capability skill to look it up — e.g. models via
[`../../cargo-storage/SKILL.md`](../../cargo-storage/SKILL.md), connectors via
[`../../cargo-connection/SKILL.md`](../../cargo-connection/SKILL.md), agents via
[`../../cargo-ai/SKILL.md`](../../cargo-ai/SKILL.md):
```bash
cargo-ai storage model list # find the model's uuid
```
## 3. Import each resource into state
```bash
cargo-ai cdk import model:contacts <model-uuid> --dir my-workspace
cargo-ai cdk import connector:hubspot <connector-uuid> --dir my-workspace
cargo-ai cdk import agent:sdr <agent-uuid> --dir my-workspace
```
- The id is `kind:slug` — the same id the plan output uses.
- **Slug-addressable kinds** (connector, model) can also self-adopt on the next
`deploy` by matching slug, so `import` is optional for them. **Uuid-only kinds**
(play, agent, capacity, territory, segment) have no slug — `import` is the only
way to bind them, and the only way to recover if `cargo.state.json` is ever lost.
## 4. Verify with a plan
```bash
cargo-ai cdk plan --dir my-workspace
```
Imported resources should show as **update** or **no-op**, not **create**. A
`create` for something that already exists means the id/slug didn't match — fix the
`define*` slug or re-import with the right uuid.
## 5. Deploy and commit
```bash
cargo-ai cdk deploy --dir my-workspace
git add cargo.state.json && git commit -m "Adopt existing workspace into CDK"
```
Imported/adopted resources are **released** (dropped from state), not deleted, if
you later `destroy` or `deploy --prune` them — the CDK won't delete something it
only adopted.
recipes/scaffold-a-workspace.md›
# Recipe: scaffold a workspace from scratch
**Use when** the user wants to stand up a new Cargo workspace as code — from a
template, reproducibly. Follow these steps as your execution plan.
## 1. Scaffold from a template
```bash
cargo-ai cdk init my-workspace # 'blank' (minimal) — the default
cargo-ai cdk init my-workspace --template full # every resource type, wired up
cargo-ai cdk init --list-templates # see all templates
```
Use `--template full` when the user wants a worked example spanning connectors,
models, plays, tools, agents, MCP, context, files, workers, and apps; `blank` when
they want to start empty.
## 2. Install and authenticate
```bash
cd my-workspace && npm install # pulls @cargo-ai/cdk + zod
cargo-ai login # authenticate + select the target workspace
cargo-ai whoami # confirm the selected workspace
```
## 3. (Optional) Generate typed config
```bash
cargo-ai cdk types # types config against this workspace's integrations
```
Not required to deploy, but it makes `defineConnector`/`defineModel` config and
`integrations.*` in workflow bodies type-check. See
[`../guides/typed-config.md`](../guides/typed-config.md).
## 4. Set secrets
Any `secret("NAME")` in the code resolves from the environment at deploy time.
Export each one first:
```bash
export HUBSPOT_API_KEY=... # matches secret("HUBSPOT_API_KEY") in connectors/hubspot.ts
```
A missing env var fails the deploy with an unresolved `${NAME}` placeholder.
## 5. Plan, then deploy
```bash
cargo-ai cdk plan # offline diff — review what will be created
cargo-ai cdk deploy # create everything, write cargo.state.json
```
`deploy` prompts for confirmation. Review the plan output first; it lists each
resource as create / update / no-op.
## 6. Commit state
```bash
git add cargo.state.json && git commit -m "Deploy initial workspace"
```
`cargo.state.json` is the link from code to the deployed resources — and the only
handle on deployed plays/agents. Commit it. The `.gitignore` scaffolded by
`cdk init` already excludes `.cargo-ai/`, the lock, backup, and audit files.
## 7. Iterate
Edit `define*` files, then `cargo-ai cdk plan` → `cargo-ai cdk deploy` again — only
what changed is applied. To tear the workspace back down:
```bash
cargo-ai cdk destroy --all
```
See [`../guides/authoring-resources.md`](../guides/authoring-resources.md) to add
resources and [`../guides/deploy-and-state.md`](../guides/deploy-and-state.md) for
prune/drift/rollback.
references/commands.md›
# Command reference — `cargo-ai cdk`
All subcommands accept `--dir <path>` (the project root, default `.`) and `--json`
(machine-readable output). Run from the project root so `.cargo-ai/` and
`cargo.state.json` land in the right place. Confirm the surface live with
`cargo-ai cdk <subcommand> --help`.
| Command | What it does |
|---|---|
| `cargo-ai cdk init <directory>` | Scaffold a project from a template. `--template <blank\|full>` (default `blank`), `--list-templates`. |
| `cargo-ai cdk types` | Generate per-workspace types into `.cargo-ai/` for typed config. |
| `cargo-ai cdk plan` | Offline: compile the graph and diff against `cargo.state.json`. No API calls. |
| `cargo-ai cdk deploy` | Create/update resources in dependency order; write state. Prompts unless `--yes`. |
| `cargo-ai cdk refresh` | Read-only: report resources that drifted from code (changed/deleted out of band). |
| `cargo-ai cdk import <id> <uuid>` | Bind an existing live resource (`kind:slug`) to a uuid in state. |
| `cargo-ai cdk rollback` | Restore `cargo.state.json` from the pre-deploy snapshot. |
| `cargo-ai cdk destroy` | Tear down resources recorded in state. `--target <id>` for one, `--all` for everything. |
## Common flags
- `--dir <path>` — project root (default `.`).
- `--yes` — skip the confirmation prompt (**required in CI / non-interactive**).
- `--json` — machine-readable output.
- `--force` — steal a stale `cargo.state.lock`.
## `deploy` modifiers
- `cargo-ai cdk deploy --prune` — also **delete** resources that are in state but
removed from code (reverse dependency order; adopted resources are released, not
deleted).
- `cargo-ai cdk deploy --refresh` — re-read live resources and re-apply your code
over any out-of-band changes.
## `destroy` targets
- `cargo-ai cdk destroy --target <kind:slug>` — remove one resource (refused if
other state resources still depend on it).
- `cargo-ai cdk destroy --all` — remove everything in state, dependents first.
## Examples
```bash
cargo-ai cdk init acme --template full # scaffold
cargo-ai cdk types --dir acme # type config
cargo-ai cdk plan --dir acme # preview
cargo-ai cdk deploy --dir acme --yes # apply (non-interactive)
cargo-ai cdk refresh --dir acme # drift report
cargo-ai cdk import agent:sdr <uuid> --dir acme # adopt a live agent
cargo-ai cdk destroy --dir acme --all --yes # tear down
```
references/cookbooks.md›
<!-- Generated by .github/scripts/sync-cookbooks.ts from gtm-skills' catalog.json. Do not edit by hand. -->
# Cookbooks: worked CDK examples, before you author one from scratch
Skills in [`getcargohq/gtm-skills`](https://github.com/getcargohq/gtm-skills) that are cookbooks: worked CDK resources, the same
jobs as the one-off skills there, as a deployed pipeline that keeps producing the result.
**Every folder is self-contained** (its own models, connectors and folders; no shared
foundation, no requires graph). The agent installing one copies it into the project as a
sibling of what is there, reconciles it with what is already declared (an existing accounts
model, an existing CRM connector), adapts it, plans, deploys on a yes, and walks its `Done
when`. The code is a worked example, not a template to fill in.
```sh
npx skills add getcargohq/gtm-skills/<slug> # then say what you want; the skill carries its own procedure
cargo-ai cdk init <dir> --template blank # only if there is no CDK project yet: the shell comes from the CLI
```
## With a skill
| Skill | Deploys | State |
| --- | --- | --- |
| `account-scoring` | Keep every account scored and tiered against your written ICP by a deployed agent that re-scores as accounts arrive and as the ICP changes, writing the rationale back to the CRM. | to-be-approved |
| `tam-building` | Stand up your account universe as a deployed pipeline: a Sales Navigator company search split past the 1,000 extraction cap, resolved to real domains, deduped into a shared accounts model. | to-be-approved |
## When one does not fit as written
Declared adaptations, not forks. Reach for one before concluding a skill is the wrong start.
**`account-scoring`**
- `deterministic-scoring` — You need fixed cost and exact reproducibility, or an LLM judgement is not acceptable to your team. Costs: The criteria move out of the ICP markdown and into code, so they stop being reviewable by non-engineers, and you lose the rationale entirely.
- `skip-crm-roundtrip` — You want the score on the model directly and do not need it visible in the CRM. Costs: Reps lose the score and rationale where they actually work. The native's input is untyped, so confirm the field shape on the first run.
- `no-crm-at-all` — You have no CRM, or you do not want to hand this skill a CRM credential. Costs: Every other CRM-dependent skill you install later brings a CRM connector of its own; reuse one.
**`tam-building`**
- `non-linkedin-source` — You do not want to source from LinkedIn at all, or Sales Nav does not cover your market. Costs: You lose the Sales Nav facet taxonomy that makes the split tactic mechanical, and the splitting has to be redesigned around the new source's own limits.
- `land-without-promoting` — You want to see and filter the raw market before paying to enrich it. Costs: Nothing reaches `accounts`, so no downstream skill (scoring, contact sourcing, signals) has anything to work with until you promote.
- `sample-first` — The market search is large and you want to see the cost curve before committing. Costs: Your TAM is deliberately incomplete until you widen it, so do not score or report on coverage from a sample.
## Routing
**One-off versus standing is the whole test.** A user who wants a list today wants a one-off
skill (or `cargo-gtm`, when this pack is installed); a user who wants a pipeline that keeps
producing it wants one of these. The same words describe both, so listen for whether the
result is meant to keep arriving.
**Never `cargo-ai cdk init --force` into a directory that is not empty.** It replaces the
project's `package.json` and reverts adapted code while `cargo.state.json` survives, so the
next plan diffs a live workspace against code nobody wrote. Copy the skill folder in as a
sibling instead; that is what its own procedure says.
references/examples/full-workspace.md›
# Example: a full GTM workspace end-to-end
This walks the `full` template (`cargo-ai cdk init <dir> --template full`) — a
complete, runnable Cargo workspace defined in code that exercises every resource
type and wires them by **handle**.
## The graph
```
hubspot (connector) ──dataset──▶ contacts (model) ──model──────────┬─▶ onboarding (play)
├─▶ sdr (agent)
openai (connector) ──connector──▶ enricher (agent) ──subAgent──▶ sdr│
└────────connector──────────────────▶ sdr ─────┘
enrich (tool, backed by a workflow) ─┐
sdr (agent) ─────────────────────────┼─▶ crm (mcpServer)
contacts (model) ────────────────────┘
playbook (file) webhook (worker) dashboard (app) context (repo)
```
## Project layout
```
my-workspace/
package.json # depends on @cargo-ai/cdk + zod
tsconfig.json # include: ["**/*.ts", ".cargo-ai/**/*.d.ts"]
.gitignore # .cargo-ai/, cargo.state.lock, cargo.state.bak.json, cargo.state.audit.jsonl
connectors/hubspot.ts # defineConnector + secret()
connectors/openai.ts # defineConnector adopt: true
folders/crm.ts # defineFolder (per-kind)
models/contacts.ts # defineModel dataset: hubspot
tools/enrich.ts # defineWorkflow + defineTool
agents/enricher.ts # defineAgent (sub-agent)
agents/sdr.ts # defineAgent (model + tool + sub-agent + trigger + evaluator)
plays/onboarding.ts # definePlay + defineWorkflow
mcp/crm.ts # defineMcpServer
context/context.ts # defineContext dir: "context" (+ context/*.md)
files/playbook.ts # defineFile (+ playbook.md)
workers/webhook.ts # defineWorker (+ webhook/ built bundle)
apps/dashboard.ts # defineApp (+ dashboard/ Vite app)
```
Importing a `.ts` file **is** registration — there is no manifest. The loader
imports every `.ts` under the project (skipping the worker/app bundle sub-dirs,
which have their own `package.json`), and each `define*` registers as a side
effect.
## A wired slice
```ts
// connectors/hubspot.ts
export const hubspot = defineConnector("hubspot", {
integration: "hubspot",
config: { method: "privateApp", accessToken: secret("HUBSPOT_API_KEY") },
});
// models/contacts.ts
export const contacts = defineModel("contacts", {
dataset: hubspot, // handle → connector deployed first, dataset injected
extractSlug: "fetchRecords",
config: { objectType: "contacts", columnSelectionMode: "all" },
folder: modelsFolder,
schedule: { type: "cron", cron: "0 * * * *" },
});
// agents/sdr.ts
export const sdr = defineAgent("sdr", {
connector: openai,
languageModel: "gpt-4o",
systemPrompt: "You qualify inbound leads and route hot ones to Slack.",
models: [{ ref: contacts, readOnly: true }],
tools: [enrich],
subAgents: [{ ref: enricher, waitUntilFinished: true }],
folder: agentsFolder,
});
```
## Deploy walkthrough
```bash
cd my-workspace && npm install
cargo-ai login # authenticate + select the workspace
cargo-ai cdk types # type defineConnector/defineModel config against this workspace
export HUBSPOT_API_KEY=... # matches secret("HUBSPOT_API_KEY")
cargo-ai cdk plan
# → lists every resource as create / update / no-op, in dependency order:
# create connector:hubspot, create connector:open_ai (adopt), create folder:crm-models, …
cargo-ai cdk deploy
# → creates each in order, writing cargo.state.json after each resource.
# Workers/apps build server-side (slower). Live URLs appear as webhook.url / dashboard.url.
git add cargo.state.json && git commit -m "Deploy full workspace"
```
Re-run `cargo-ai cdk deploy` after editing a file — only the changed resource is
applied. Tear it all down with `cargo-ai cdk destroy --all`.
Secrets referenced with `secret("HUBSPOT_API_KEY")` resolve from the environment at
deploy time and stay out of the content hash — only `{hash, uuid, outputs}` land in
`cargo.state.json`, never secret values.
references/resources.md›
# Resource reference
Every builder is imported from `@cargo-ai/cdk`, takes `(slug, spec)` (except
`defineContext`, which takes only a spec — it's a workspace singleton), and returns
a **handle** carrying deferred output tokens (`uuid`, and for connectors
`datasetUuid`; for workers/apps `url`). Wire resources by passing a handle where a
reference is expected; use `xxRef("uuid")` for a resource not defined in code.
The tables below list the **commonly used** spec fields — the TypeScript types on
each builder are the source of truth for the complete set. Fields that take a
**ref** accept a handle or an `xxRef` (and `{ ref, …options }` when they carry
per-call options).
## Builders
| Builder | Purpose | Key spec fields | Ref fields | Outputs |
|---|---|---|---|---|
| `defineConnector(slug, spec)` | Data source or LLM provider | `integration`, `config` (typed per integration), `adopt?`, `rateLimit?`, `cacheTtlMilliseconds?` | — | `uuid`, `datasetUuid` (data connectors) |
| `defineModel(slug, spec)` | Table sourced from a connector's dataset | `dataset`, `extractSlug`, `config`, `schedule?`, `folder?` | `dataset` (connector/dataset), `folder` | `uuid` |
| `defineTool(slug, spec)` | Tool backed by a workflow | `workflow`, `description?`, `emojiSlug?`, `triggers?`, `folder?` | `folder` | `uuid` |
| `definePlay(slug, spec)` | Per-row automation over a model | `model`, `workflow`, `changeKinds`, `runCreationRule`, `schedule`, `folder?` | `model`, `folder` | `uuid` |
| `defineAgent(slug, spec)` | AI agent | `connector`, `languageModel`, `systemPrompt`, `models?`, `tools?`, `subAgents?`, `connectorActions?`, `capabilities?`, `maxSteps?`, `triggers?`, `evaluator?`, `color?`, `folder?` | `connector`, `models`, `tools`, `subAgents`, `folder` | `uuid` |
| `defineMcpServer(slug, spec)` | MCP endpoint bundling resources | `description?`, `tools?`, `agents?`, `models?`, `folder?` | `tools`, `agents`, `models`, `folder` | `uuid` |
| `defineFolder(slug, spec)` | Per-kind folder | `kind`, `name`, `parent?` | `parent` | `uuid` |
| `defineFile(slug, spec)` | Content file from a local path | `path`, `name`, `folder?` | `folder` | `uuid` |
| `defineContext(spec)` | Workspace context repo (singleton) | `dir?`, `files?` | — | `uuid` |
| `defineSegment(slug, spec)` | Saved view over a model | `model` (immutable), `filter` (required) | `model` | `uuid` |
| `defineCapacity(slug, spec)` | Revenue-org capacity | `model`, `color?`, `description?`, member capacity fields | `model`, members | `uuid` |
| `defineTerritory(slug, spec)` | Revenue-org territory | `model`, `members`, `color?`, `description?`, `fallbackMember?` | `model`, `members` | `uuid` |
| `defineWorker(slug, spec)` | Hosted worker (built bundle) | `path`, `description?`, `folder?` | `folder` | `uuid`, `url` |
| `defineApp(slug, spec)` | Hosted Vite SPA | `path`, `description?`, `folder?` | `folder` | `uuid`, `url` |
| `defineDomain(name, spec)` | Sending domain + its DNS zone | `adopt?`, `dnsRecords?` (**replaces the whole zone**) | — | `uuid` |
| `defineMailbox(slug, spec)` | Sending inbox on a domain (**monthly credit charge**) | `domain`, `type` (`google`/`shared`/`private` — no `outlook`), `username?` (defaults to slug), `firstName`, `lastName`, `signature?`, `folder?`, `adopt?` | `domain`, `folder` | `uuid` |
| `defineAlert(slug, spec)` | Scheduled threshold alert (observability) | `schedule`, `scope`, `threshold`, `actions`, `name?`, `description?`, `enabled?`, `folder?` | scope: `workflow`/`connector`/`tool`/`agent`/`model`; each action's `ref`; `folder` | `uuid` |
`defineWorkflow(slug, { input, output, uses? }, build)` is re-exported from
`@cargo-ai/cdk` for `defineTool`/`definePlay` bodies — see
[`../guides/authoring-resources.md`](../guides/authoring-resources.md).
## Ref helpers
From `@cargo-ai/cdk`: `connectorRef`, `datasetRef`, `modelRef`, `folderRef`,
`playRef`, `memberRef`. From the workflow SDK (re-exported): `toolRef`, `agentRef`.
Each takes a uuid string and returns a kind-branded handle:
```ts
import { defineModel, connectorRef, folderRef } from "@cargo-ai/cdk";
export const leads = defineModel("leads", {
dataset: connectorRef("6f0c…"), // existing connector, by uuid
extractSlug: "fetchRecords",
folder: folderRef("a1b2…"),
});
```
## Notes on specific fields
- **`defineConnector` `config`** is a per-integration shape — a discriminated union
for auth (e.g. HubSpot `method: "privateApp" | "oauth"`). `secret()` is accepted
only on credential/encryption fields. Run `cargo-ai cdk types` to type it (see
[`../guides/typed-config.md`](../guides/typed-config.md)).
- **`adopt: true`** on `defineConnector` links an existing authenticated connector
by slug instead of creating one — for OAuth/key connectors you can't declare.
- **`defineModel` `dataset`** takes the **connector** handle (its dataset uuid is
injected) or a `datasetRef`/`connectorRef`.
- **`schedule`** shapes: `{ type: "cron", cron: "0 * * * *" }` or
`{ type: "watch" }` (plays react to row changes).
- **`definePlay` `changeKinds`**: `["added", "updated", …]`; `runCreationRule`:
e.g. `"always"`.
- **`defineFile` `path`** and **`defineWorker`/`defineApp` `path`** point at local
files/dirs, typically via `new URL("./x", import.meta.url).pathname`. File content
is hashed at define time, so edits show as drift. Worker `path` must be a **built**
bundle dir (`index.js` + `manifest.json` + `package.json` + `package-lock.json`).
- **`defineAlert` `scope` + `threshold`** are a **matched pair** — TS narrows the
threshold menu by `scope.kind`: `spans`/`runs`/`records` take the telemetry metrics
(`errorRate`, `duration`+`aggregation`, `credits`+`aggregation`, `count`), `model`
takes `recordsCount`/`recordsShare`/`freshness`/`syncDuration`, and
`orchestrationQuery`/`storageQuery` take `{ operator, value }` (the query computes
the value, so no metric). The scope wires the watched resource **by handle**
(`workflow:` a `definePlay`/`defineTool` handle or `workflowRef`, plus `connector`/
`tool`/`agent`/`model`), so the reconciler deploys the producer first and injects
its uuid.
- **`defineAlert` `actions`** fire as runs on breach. Each is a `{ ref, config }`
wrapper (`config` required — an alert fires unattended, so a missing input is a type
error, not a silent `{}`). Prefer the typed helpers `alertConnectorAction({ ref:
slack.actions.postMessage, config })` / `alertToolAction({ ref: enrich, config })` —
`config` is checked against the action/tool input schema (connector schemas need
`cargo-ai cdk types` to have run) — or a bare `{ ref: agent, config, release?,
waitUntilFinished? }`. Every `config` leaf accepts a `{{ … }}` template
(`{{event.value}}`, `{{alert.name}}`, `{{alert.url}}`, …) interpolated against the
firing context. Like a play, an alert has **no author-set wire slug** — its identity
on redeploy is the state uuid, so committing `cargo.state.json` is what keeps it
addressable. Scope/threshold matrix, metric units, and firing semantics:
[`../../cargo-observability/SKILL.md`](../../cargo-observability/SKILL.md).
references/troubleshooting.md›
# Troubleshooting
## `✗ Deploy failed: connector:<slug>: Invalid configuration`
The connector's `config` doesn't match the integration's schema. Most often the
credential wasn't wrapped in `secret()` — a data connector's credential field
expects an encryption envelope, and `secret("ENV_VAR")` produces it. Fix:
```ts
config: { method: "privateApp", accessToken: secret("HUBSPOT_API_KEY") }, // not a bare string
```
Run `cargo-ai cdk types` so the config type-checks against the real schema at
author time and surfaces the required shape (see
[`../guides/typed-config.md`](../guides/typed-config.md)). The deploy error now also
surfaces the API's structured detail (which field, the reason) — read past the
terse "Invalid configuration" summary.
## `unresolved placeholder "${NAME}"`
A `secret("NAME")` or `env("NAME")` had no matching environment variable at deploy.
The CDK refuses to send a literal `${NAME}` to the API. Export it first:
```bash
export NAME=...
cargo-ai cdk deploy
```
## Deploy refuses: workspace mismatch
`cargo.state.json` records the workspace it was deployed to; `deploy`/`destroy`
refuse when that ≠ the currently selected workspace (a guard against reconciling a
dev definition into prod). Select the right workspace at `login`, or use a separate
state file per environment.
## `.cargo-ai/` or `cargo.state.json` landed in the wrong directory
`npx`/`cargo-ai` resolve from the **nearest `package.json`**, not your shell's cwd.
Run `cdk` commands from the project root, or pass `--dir <project-root>` explicitly.
## `integrations.<slug>` is `any` / not callable, or `config` isn't type-checked
Types aren't generated or aren't wired in. Run `cargo-ai cdk types`, ensure
`tsconfig.json` `include` has the explicit glob `".cargo-ai/**/*.d.ts"` (a bare
`.cargo-ai` dot-dir is ignored by TypeScript), and `import "./.cargo-ai/cargo-register.js";`
at the top of workflow modules that use `integrations.*`. Re-run `cdk types` after
changing workspace integrations.
## `could not parse the workflow body`
A `defineWorkflow` body must be a supported JS subset — it's **parsed, not
executed**. Remove `await`, `throw`, `try/catch`, closures over outer variables,
and destructuring; compile workflow files with a modern target (ES2022+) and don't
instrument them with coverage tools (they rewrite the function source). Use
`js(({ nodes }) => …)` for logic outside the supported subset.
## Deploy is slow / seems to hang on a worker or app
Workers and apps **build server-side** — the reconciler uploads the bundle, waits
for the build, and promotes. This is expected to take longer than other resources.
Ensure the worker bundle dir has a built `index.js` (+ `manifest.json`,
`package.json`, `package-lock.json`) before deploying.
## A play or agent got orphaned (state lost)
Plays and agents have no slug, so `cargo.state.json` is the only link to them.
**Commit the state file.** If it's lost, find the live uuid via the matching
capability skill and rebind: `cargo-ai cdk import agent:<slug> <uuid>`. Never
delete `cargo.state.json` to "start clean" — you'll orphan every play/agent it
tracked.
## `plan` shows `create` for a resource that already exists
Its `kind:slug` id didn't match state. Either the slug in the `define*` changed, or
you migrated a workspace without importing — bind it: `cargo-ai cdk import <kind:slug> <uuid>`
(see [`../recipes/migrate-existing-workspace.md`](../recipes/migrate-existing-workspace.md)).
## Still stuck
File a report so the Cargo team sees it:
`cargo-ai workspaceManagement report create --title "<summary>" --description "<commands tried, errorMessage, expected vs actual>"`
— see [`../../cargo-workspace-management/SKILL.md`](../../cargo-workspace-management/SKILL.md).
skill-metadata.json›
{
"$comment": "Generated by .github/scripts/skills-metadata.mjs — do not hand-edit. Regenerate with: node .github/scripts/skills-metadata.mjs --write .",
"name": "cargo-cdk",
"version": "1.2.3",
"documents": [
{
"path": "SKILL.md",
"kind": "entrypoint",
"title": "Cargo CDK — declarative workspace-as-code"
},
{
"path": "guides/authoring-resources.md",
"kind": "guide",
"title": "Authoring resources"
},
{
"path": "guides/deploy-and-state.md",
"kind": "guide",
"title": "Deploy & state"
},
{
"path": "guides/typed-config.md",
"kind": "guide",
"title": "Typed config — `cargo-ai cdk types`"
},
{
"path": "recipes/add-connector-and-model.md",
"kind": "recipe",
"title": "Recipe: add a connector and a model sourced from it"
},
{
"path": "recipes/build-an-agent.md",
"kind": "recipe",
"title": "Recipe: build an agent (model + tool + agent)"
},
{
"path": "recipes/deploy-from-ci.md",
"kind": "recipe",
"title": "Recipe: deploy from CI"
},
{
"path": "recipes/migrate-existing-workspace.md",
"kind": "recipe",
"title": "Recipe: bring an existing workspace under CDK management"
},
{
"path": "recipes/scaffold-a-workspace.md",
"kind": "recipe",
"title": "Recipe: scaffold a workspace from scratch"
},
{
"path": "references/commands.md",
"kind": "reference",
"title": "Command reference — `cargo-ai cdk`"
},
{
"path": "references/cookbooks.md",
"kind": "reference",
"title": "Cookbooks: worked CDK examples, before you author one from scratch"
},
{
"path": "references/examples/full-workspace.md",
"kind": "example",
"title": "Example: a full GTM workspace end-to-end"
},
{
"path": "references/resources.md",
"kind": "reference",
"title": "Resource reference"
},
{
"path": "references/troubleshooting.md",
"kind": "reference",
"title": "Troubleshooting"
}
],
"contentHash": "9bab53eda7c82d2c830dfb5ed20f8eee3ca499e86bb8cb7a01c38a95664e121f"
}
SKILL.md›
---
name: cargo-cdk
description: "Manage a whole Cargo workspace as code — declare connectors, models, plays, tools, agents, MCP servers, segments, context, folders, files, workers, and apps in TypeScript, then reconcile them with `cargo-ai cdk` (init → types → plan → deploy), the way you would run Pulumi or the AWS CDK. Triggers: \"as code\", \"in git\", \"version-controlled\", \"reproducible\", \"Terraform for Cargo\", \"set up a whole workspace\", \"staging and production\", \"deploy from CI\", \"review this in a PR\", \"cargo.state.json\", \"scaffold from a template\", \"is there a cookbook for this\", \"start from a cookbook\". Skills with a CDK example (TAM building, account scoring, contact sourcing, routing, AI SDR, rep cockpit) live in gtm-skills; menu in references/cookbooks.md. Skip when: it is a one-off operation, a read, or an ad-hoc query — use the matching capability skill."
version: "1.2.3"
compatibility: Requires @cargo-ai/cli (npm). Sign in or create an account with `cargo-ai login --email` (emailed code, no browser), `--oauth`, or an API token
homepage: https://github.com/getcargohq/cargo-skills
metadata:
author: getcargo
openclaw:
requires:
bins:
- cargo-ai
install:
- kind: node
package: "@cargo-ai/cli@latest"
bins:
- cargo-ai
homepage: https://github.com/getcargohq/cargo-skills
---
# Cargo CDK — declarative workspace-as-code
Use this skill to define a Cargo workspace in TypeScript (`define*` builders from
`@cargo-ai/cdk`) and reconcile it to live infrastructure with `cargo-ai cdk deploy`.
It is the **declarative** counterpart to the imperative capability skills: instead
of running one CLI command per resource, you write the whole graph once and deploy
it repeatably, with a committed `cargo.state.json` linking your code to what Cargo
created.
## Bootstrap
Already signed in (`cargo-ai whoami` returns a workspace)? Skip to the next section.
```bash
npm install -g @cargo-ai/cli # no global install? prefix every command with `npx @cargo-ai/cli`
cargo-ai login --email [email protected] # emailed code, no browser; creates the account on first use
# alternatives: --oauth (browser) · --token <api-token> (CI)
cargo-ai whoami # confirm the active workspace before any write
cargo-ai cdk --help # `unknown command` = CLI too old; reinstall @cargo-ai/cli@latest
```
Two CDK-specific extras: the project needs **`@cargo-ai/cdk` as a dependency** for the `define*` builders you import (`cargo-ai cdk init` scaffolds a `package.json` with it — then `npm install`), and the `cargo-ai cdk` domain ships with the CLI itself.
Every command prints JSON to stdout; failures exit non-zero with `{"errorMessage": "..."}`. Anything that creates a run or a batch is async — pass `--wait-until-finished` or poll the matching `get`. When the full skill bundle is installed, [`../cargo/references/prerequisites.md`](../cargo/references/prerequisites.md) adds the CLI version pin, token scopes, and the admin-only surface.
## 1) What this skill governs
- **Authoring** every Cargo resource with a `define*` builder that returns a
**handle**; wiring resources by passing handles to each other (the dependency
graph is your variable graph).
- **Deploying** the graph: `plan` (offline diff) → `deploy` (create/update, write
state) → `destroy` (tear down). Plus drift (`refresh`), adoption (`import`), and
recovery (`rollback`).
- **Typing** the config against your workspace's real integration schemas
(`cargo-ai cdk types`).
The CDK spans **every** resource kind — so it overlaps every imperative capability
skill (`cargo-connection`, `cargo-storage`, `cargo-ai`, `cargo-orchestration`,
`cargo-content`, `cargo-hosting`, …). Which to reach for is the first decision:
## 2) CDK or the CLI? — the routing decision
> **Declarative (this skill) vs imperative (a capability skill).**
Use the **CDK** when the user is **managing resources as an artifact**:
- "Set up / stand up / bootstrap a whole workspace (as code / from a template)."
- "Make this reproducible / version-controlled / in git / repeatable across
environments (dev → prod)."
- "Deploy these connectors + models + agents together" (a multi-resource graph
wired by dependency).
- Anything that should be re-runnable and diffable, where losing the definition
would be a problem.
Use the matching **capability skill** (imperative `cargo-ai <domain>`) when the
user is doing a **one-off operation** or **exploring**:
- "Create one connector", "add a column to this model", "list connectors",
"run this workflow", "query storage", "read this agent's memory."
- Any read, ad-hoc query, or single mutation that doesn't need to live in code.
When unsure, ask whether the result should be committed and re-deployable. If yes
→ CDK. If it's a quick action or a read → the capability skill (see the
[`cargo` router](../cargo/SKILL.md) to pick the right domain).
## 3) The lifecycle
```
cargo-ai cdk init <dir> scaffold a project from a template (blank | full)
│
cargo-ai cdk types generate per-workspace types for typed config (optional)
│
(author define* files) importing a .ts file IS registration — no manifest
│
cargo-ai cdk plan offline: compile the graph, diff against cargo.state.json
│
cargo-ai cdk deploy create/update resources in dependency order, write state
│
cargo-ai cdk destroy tear down resources recorded in state
```
> **`cdk plan` says what resources change; it doesn't show what a play does.**
> For a `definePlay` / `defineTool` graph past three nodes, present a Mermaid
> flowchart of the node graph alongside the plan — routing, fallbacks, and which
> nodes bill on every scheduled run are what the reviewer is approving. Generate it
> from the deployed release after the first deploy, or from the node array while
> authoring:
> [`../cargo-orchestration/references/node-diagram.md`](../cargo-orchestration/references/node-diagram.md).
Side branches: `cargo-ai cdk refresh` (read-only drift report) · `deploy --refresh`
(re-apply code over out-of-band edits) · `deploy --prune` (delete resources removed
from code) · `cargo-ai cdk import <id> <uuid>` (bind an existing live resource into
state) · `cargo-ai cdk rollback` (restore the pre-deploy state snapshot).
## 4) Documentation hierarchy
- **Level 1** — `SKILL.md` (this file): the decision model, lifecycle, critical
rules, and routing.
- **Level 2** — Guides:
[`guides/authoring-resources.md`](guides/authoring-resources.md),
[`guides/deploy-and-state.md`](guides/deploy-and-state.md),
[`guides/typed-config.md`](guides/typed-config.md).
- **Level 2.5** — Recipes: [`recipes/*.md`](recipes/) — step-by-step playbooks to
follow as your execution plan.
- **References** — [`references/resources.md`](references/resources.md) (the full
builder catalog), [`references/commands.md`](references/commands.md) (every
`cargo-ai cdk` subcommand + flags),
[`references/troubleshooting.md`](references/troubleshooting.md), and
[`references/examples/full-workspace.md`](references/examples/full-workspace.md).
## 5) Read behavior — match the task to a doc and READ IT
| When the task involves… | Read this first | What it gives you |
|---|---|---|
| Writing `define*` files, wiring resources, `secret()`/`env()`, `defineWorkflow` bodies (tool/play logic) | [`guides/authoring-resources.md`](guides/authoring-resources.md) | The builder catalog, the handle/ref model, secrets, and how workflow bodies compile. |
| `plan` / `deploy` / `destroy`, the state file, drift, adopting existing resources, CI | [`guides/deploy-and-state.md`](guides/deploy-and-state.md) | The deploy lifecycle, `cargo.state.json` semantics, drift/import/rollback, async builds. |
| Typed config, `cargo-ai cdk types`, tsconfig wiring, `integrations.*` in workflow bodies | [`guides/typed-config.md`](guides/typed-config.md) | What `cdk types` generates and how to wire it into your project. |
| A field/spec/output for a specific builder | [`references/resources.md`](references/resources.md) | Every builder → spec fields → which ref each takes → outputs. |
| Exact command flags | [`references/commands.md`](references/commands.md) | Every `cargo-ai cdk` subcommand and its flags. |
| A deploy error / footgun | [`references/troubleshooting.md`](references/troubleshooting.md) | The known failure modes and fixes. |
| A known GTM outcome, before authoring one | [`references/cookbooks.md`](references/cookbooks.md) | The cookbook menu: gtm-skills that carry a worked CDK example, and the adaptations each supports. |
### Cookbooks — check the menu before authoring a known outcome from scratch
[`getcargohq/gtm-skills`](https://github.com/getcargohq/gtm-skills) holds, beside its
one-off skills, **cookbooks**: skills that carry worked CDK resources, the same job as a deployed
pipeline that keeps producing the result (TAM building, account scoring, contact
sourcing, routing engine, AI SDR, rep cockpit, …). Every folder is self-contained: its
own models, connectors and folders, no shared foundation, no requires graph.
**The menu is local: [`references/cookbooks.md`](references/cookbooks.md).** Read it
before authoring a common GTM outcome from scratch. It is generated from gtm-skills'
`catalog.json`, so it cannot drift.
**A cookbook is a worked example, not a template to fill in.** Each one declares in its `SKILL.md` what may be reshaped, what must hold or it stops
working, and what has to be answered either way, and it carries its own procedure:
look at the repo, `cargo-ai cdk init --template blank` if there is no CDK project yet,
copy the folder in as a sibling and reconcile it with what is already declared, adapt,
plan and stop, deploy on a yes, walk its `Done when`. There is no scaffolder or copy
tool in the middle: **you place the code**, because you can see the project and a tool
cannot.
```sh
npx skills add getcargohq/gtm-skills/tam-building # then: "keep our TAM current"
cargo-ai cdk init my-project --template blank # only if there is no CDK project yet
```
**If you are mid-task and the skill is not in this session**, run the `skills add`
above and read `.agents/skills/<slug>/SKILL.md` directly; no reload needed. To read
one without installing, `npx skills use getcargohq/gtm-skills@<slug>` prints it.
**Routing rule: one-off versus standing.** A user who wants the list today wants
`cargo-gtm` (or gtm-skills' one-off `build-tam-list`); a user who wants a pipeline
that keeps producing it wants `tam-building`. The same words describe both ("build
our TAM"), so listen for whether the result is meant to keep arriving. A cookbook
matches → install it and follow it. No match → author from the recipes below.
**Never `cargo-ai cdk init --force` into a directory that is not empty.** It replaces
the project's `package.json` and reverts adapted code, while `cargo.state.json`
survives, so the next `plan` diffs a live workspace against code nobody wrote. Copy the
skill folder in as a sibling instead.
Caveat: the examples typecheck, but they are not yet deploy-verified against a live
workspace, and every one is `to-be-approved`. Treat each skill's `Done when` as the
acceptance test, and always review `cargo-ai cdk plan` before deploying.
### Recipes — follow step-by-step when one matches
| Recipe | Use when… |
|---|---|
| [`recipes/scaffold-a-workspace.md`](recipes/scaffold-a-workspace.md) | Standing up a new workspace from scratch (`init --template full` → types → plan → deploy). |
| [`recipes/add-connector-and-model.md`](recipes/add-connector-and-model.md) | Adding a data source + a model sourced from it, wired by handle. |
| [`recipes/build-an-agent.md`](recipes/build-an-agent.md) | Composing a model + tool + agent (with `uses` / `models` / `tools`) and deploying. |
| [`recipes/migrate-existing-workspace.md`](recipes/migrate-existing-workspace.md) | Bringing an already-live workspace under CDK management via `cdk import`. |
| [`recipes/deploy-from-ci.md`](recipes/deploy-from-ci.md) | Deploying non-interactively from CI (token auth + committed state). |
## 6) Critical rules
- **Commit `cargo.state.json`.** It is the link from your code to the resources
Cargo created — and the **only** handle on a deployed **play**, **agent**, or
**alert** (they have no slug). Lose it and those resources orphan; recover a link
with `cargo-ai cdk import`. It records only `{hash, uuid, outputs}` — never secret
values. Git-ignore the working files (`cdk init` scaffolds this):
```gitignore
.cargo-ai/
cargo.state.lock
cargo.state.bak.json
cargo.state.audit.jsonl
```
- **Secrets:** wire credentials with `secret("ENV_VAR")` (often
`secret("HUBSPOT_API_KEY")`). The value is read from the environment **at deploy
time**, kept out of the content hash and out of state, so rotating a token
doesn't read as drift. Export the env var before deploying — a missing one fails
the deploy with an unresolved `${ENV_VAR}` placeholder.
- **Wire by handle, never by `.uuid`.** Pass a `define*` handle directly
(`dataset: hubspot`, `tools: [enrich]`), or `xxRef("uuid")` for a resource you
didn't define in code (`connectorRef`, `modelRef`, `folderRef`, `toolRef`,
`agentRef`, …). Where a reference needs per-call options, wrap it as
`{ ref, …options }` (e.g. `models: [{ ref: contacts, readOnly: true }]`).
- **Run `cargo-ai cdk types` after workspace integrations change** — it
regenerates `.cargo-ai/` so `defineConnector`/`defineModel` config (and
`integrations.*` in workflow bodies) type-check against the real schemas. Typing
is a bonus, never a gate: deploy works without it.
- **Run `cdk` commands from the project root.** `npx`/`cargo-ai` resolve from the
nearest `package.json`; run elsewhere and `.cargo-ai/` and `cargo.state.json`
land in the wrong directory. Use `--dir <path>` to be explicit.
- **`--yes` in CI.** `deploy` and `destroy` prompt for confirmation; non-interactive
runs must pass `--yes`.
- **A `definePlay`/`defineTool` graph with paid nodes gets a sample run before it
goes wide.** Deploying is not running, but the first thing that runs a deployed
play is usually a batch over the whole segment — and a scheduled play re-bills
every node on every run. Before enrolling everything (or enabling a schedule),
run the deployed workflow on **10–20 records** — `cargo-ai orchestration batch
create --data '{"kind":"filter","modelUuid":"…","filter":…,"limit":15}'`, or
`batch create --file ./plays/x.ts` to test-run the module without deploying —
then ask the user to approve the full enrollment with the **record count** and
**credit estimate**. Read the provider's playbook
(`../cargo-gtm/provider-playbooks/<slug>.md`, esp. its *Recurring use* section)
and the gate in
[`../cargo-gtm/references/cost-discipline.md`](../cargo-gtm/references/cost-discipline.md).
- **A `defineAlert` whose actions call paid nodes re-bills on every breach.** An
alert's `actions` fire as real runs, so a badly-sized `threshold` on a tight
`schedule` can breach — and bill — every tick. Size the threshold with
`cargo-ai observability alert preview` before deploying, prefer cheap notification
actions (an agent that posts, a connector notification) over anything that fans
out, and apply the same cost gate above when an action calls a credits-based
provider. Scope/threshold and firing semantics:
[`../cargo-observability/SKILL.md`](../cargo-observability/SKILL.md).
- **`defineMailbox` bills monthly, and `defineDomain` rewrites a DNS zone.** A
mailbox is 100–160 credits *per month* for as long as it exists (`cargo-ai
mailboxManagement pricing get` for live figures), so a `+ create mailbox:…` line
in the plan is a recurring charge the user approves, not a one-off. Its `domain`,
`username` and `type` are **create-only** — changing any of them is destroy +
recreate, i.e. a brand-new inbox back at the bottom of a 45-day warm-up ramp. The
deploy polls `refreshStatus` for up to 5 minutes waiting for `active`. On
`defineDomain`, `dnsRecords` is the **whole zone, not a patch**: declaring it
replaces every live record (including the ones the registrar wrote at purchase),
and omitting it leaves the zone untouched. Use `adopt: true` for a domain or
mailbox bought in the UI. Ramp, suppression and sending:
[`../cargo-mailbox-management/SKILL.md`](../cargo-mailbox-management/SKILL.md).
- **Route CDK-managed resources into a clearly-labelled folder.** Set `folder:` on
each builder so everything CDK owns lands in a dedicated folder whose name signals
"owned by code — don't hand-edit" to anyone in the UI (manual UI edits read back as
drift on the next `plan`). Folders are per-kind, so give each kind its own but share
one short, recognizable prefix — recommended: **`🔒 CDK`** (e.g. `🔒 CDK Models`,
`🔒 CDK Agents`). Keep names short (long labels truncate in the folder tree); the
lock emoji is the "don't touch" cue. See
[`guides/authoring-resources.md`](guides/authoring-resources.md).
## Help
- `cargo-ai cdk --help` and `cargo-ai cdk <subcommand> --help` for the live flag
surface.
- When a documented command/flag/response doesn't match what you observe, file a
report: `cargo-ai workspaceManagement report create` (see
[`../cargo-workspace-management/SKILL.md`](../cargo-workspace-management/SKILL.md)).