返回 Skills 目录
getcargohq/cargo-skills包含需要注意的行为

SKILL DETAIL

cargo-storage

getcargohq/cargo-skills/cargo-storage

cargo-storage 技能用于处理 Cargo 工作区中的数据层。它允许你检查和管理模型(如公司、联系人、交易等)、数据集、列、关系、记录,并对工作区存储运行 SQL 查询。 该技能支持列出和获取模型、查看 DDL、创建和更新模型、管理列(包括创建、更新、删除和重新排序)、列出和设置关系、配置统一化,以及执行 SQL 查询。它还支持通过 webhook 接收数据的摄取模型。

安装量 · 160查看来源

Installation

npx skills add https://github.com/getcargohq/cargo-skills --skill cargo-storage

技能文件

SKILL.md

最近同步 · 2026年9月2日

references/examples/columns.md›
# Column examples

## List columns for a model

```bash
cargo-ai storage column list --model-uuid <uuid>
```

Response includes `uuid`, `slug`, `type`, `label`, and `position` for each column.

## Create a string column

```bash
cargo-ai storage column create \
  --model-uuid <uuid> \
  --column '{"slug":"website_url","type":"string","label":"Website URL","kind":"custom"}'
```

## Create a number column

```bash
cargo-ai storage column create \
  --model-uuid <uuid> \
  --column '{"slug":"arr","type":"number","label":"Annual Recurring Revenue","kind":"custom"}'
```

## Create a date column

```bash
cargo-ai storage column create \
  --model-uuid <uuid> \
  --column '{"slug":"last_contacted_at","type":"date","label":"Last Contacted At","kind":"custom"}'
```

## Create a boolean column

```bash
cargo-ai storage column create \
  --model-uuid <uuid> \
  --column '{"slug":"is_customer","type":"boolean","label":"Is Customer","kind":"custom"}'
```

## Create a computed column

Computed columns derive their value from an expression over other columns.

```bash
cargo-ai storage column create \
  --model-uuid <uuid> \
  --column '{"slug":"full_name","type":"string","label":"Full Name","kind":"computed","expression":{"kind":"jsExpression","expression":"{{record.first_name}} {{record.last_name}}","instructTo":"none","fromRecipe":false},"columnsUsed":["first_name","last_name"]}'
```

`columnsUsed` is optional but recommended for dependency tracking.

## Create a metric column

Metric columns aggregate data from a related model via a relationship.

```bash
cargo-ai storage column create \
  --model-uuid <uuid> \
  --column '{"slug":"total_deals","type":"number","label":"Total Deals","kind":"metric","relationshipUuid":"<relationship-uuid>","aggregation":{"function":"count","columnSlug":"uuid"}}'
```

With an optional filter:

```bash
cargo-ai storage column create \
  --model-uuid <uuid> \
  --column '{"slug":"open_deals","type":"number","label":"Open Deals","kind":"metric","relationshipUuid":"<relationship-uuid>","aggregation":{"function":"count","columnSlug":"uuid"},"filter":{"conjonction":"and","groups":[{"conjonction":"and","conditions":[{"kind":"string","slug":"status","operator":"is","value":"open"}]}]}}'
```

## Create a lookup column

Lookup columns pull a field value from a related model via a join.

```bash
cargo-ai storage column create \
  --model-uuid <uuid> \
  --column '{"slug":"company_name","type":"string","label":"Company Name","kind":"lookup","join":{"toModelUuid":"<company-model-uuid>","fromColumnSlug":"company_uuid","toColumnSlug":"uuid"},"extractColumnSlug":"name"}'
```

With an optional filter:

```bash
cargo-ai storage column create \
  --model-uuid <uuid> \
  --column '{"slug":"primary_contact_email","type":"string","label":"Primary Contact Email","kind":"lookup","join":{"toModelUuid":"<contacts-model-uuid>","fromColumnSlug":"uuid","toColumnSlug":"company_uuid"},"extractColumnSlug":"email","filter":{"conjonction":"and","groups":[{"conjonction":"and","conditions":[{"kind":"boolean","slug":"is_primary","operator":"isTrue"}]}]}}'
```

## Update a column

Pass the full column object via `--column`. Columns are identified by `slug` (no UUID).

```bash
cargo-ai storage column update \
  --model-uuid <uuid> \
  --column '{"slug":"website_url","type":"string","label":"Website","kind":"custom"}'
```

## Remove a column

```bash
cargo-ai storage column remove --model-uuid <uuid> --column-slug website_url
```

## Reorder a column

Move a column to a specific position index (0-based).

```bash
cargo-ai storage column reorder --model-uuid <uuid> --column-slug website_url --to-index 2
```

## Column types reference

| Type      | Use for                  |
| --------- | ------------------------ |
| `string`  | Text, names, URLs, slugs |
| `number`  | Counts, amounts, scores  |
| `boolean` | Flags, yes/no values     |
| `date`    | Timestamps, dates        |
| `object`  | Nested JSON objects      |
| `array`   | Lists of values          |
| `vector`  | Embedding vectors        |
| `any`     | Untyped / mixed values   |

## Column kinds reference

| Kind       | Use for                                                     | Required extra fields                                                                                    |
| ---------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `custom`   | User-defined fields                                         | —                                                                                                        |
| `computed` | Values derived from an expression over other columns        | `expression`; optionally `columnsUsed`                                                                   |
| `metric`   | Aggregated values from a related model                      | `relationshipUuid`, `aggregation.function`, `aggregation.columnSlug`; optionally `filter`                |
| `lookup`   | A single field value pulled from a related model via a join | `join.toModelUuid`, `join.fromColumnSlug`, `join.toColumnSlug`, `extractColumnSlug`; optionally `filter` |

Column `slug` values are used in filter conditions (see `cargo-orchestration` skill's `references/filter-syntax.md`) and in `storage query execute` SQL queries.
references/examples/datasets.md›
# Dataset examples

## List all datasets

Datasets group related models together.

```bash
cargo-ai storage dataset list
```

Response includes `uuid`, `name`, and `slug` for each dataset.

## Get a specific dataset

```bash
cargo-ai storage dataset get <dataset-uuid>
```

## List models in a dataset

```bash
cargo-ai storage model list --dataset-uuid <dataset-uuid>
```

## Discover workspace data structure

Full flow to understand how data is organized:

```bash
# 1. List all datasets
cargo-ai storage dataset list
# → Note the dataset UUIDs and slugs

# 2. For each dataset, list its models
cargo-ai storage model list --dataset-uuid <dataset-uuid>
# → See which models (tables) belong to each dataset

# 3. Inspect a model's columns
cargo-ai storage model get <model-uuid>
# → See column slugs and types for each model
```

The dataset `slug` appears in DDL table names (e.g. `datasets_default` for the dataset with slug `default`).
references/examples/ingest-webhook.md›
# Ingest models — get the webhook URL and POST records

Some models are fed by **pushing** records to Cargo instead of Cargo pulling them.
Their extractor has `mode.kind === "ingest"` — the canonical one is the `http`
integration's `listenHook` ("Listen webhook"), but the same mechanism backs
`storeleads.listenList`, `rb2b.listenProfiles`, `albacross.listenWebsiteVisits`,
and others.

The app shows a **Webhook URL** on the model's settings screen. There is **no CLI
command and no API field that returns it** — the app builds the string client-side.
You can build the exact same string from data the CLI already exposes.

## The URL

```
<baseUrl>/v1/models/<model-uuid>/records/ingest?token=<api-token>
```

- `<baseUrl>` — `cargo-ai whoami` → `.baseUrl` (e.g. `https://api.getcargo.io`).
  Note the path is `/v1/models/...`, **not** `/v1/storage/models/...`.
- `<model-uuid>` — the ingest model's UUID.
- `<api-token>` — any workspace API token. The token may carry **zero
  permissions**; this route is explicitly allowed for permission-less tokens so
  the URL can be handed to a third-party system safely. The app auto-creates one
  named `Quick access` for exactly this.

`token` can also be sent as an `Authorization: Basic <token>` header instead of a
query param — preferable when the receiving system supports custom headers, since
a query param lands in logs.

## Derive it

```bash
# 1. Find the model and confirm it is an ingest model
cargo-ai storage model get <model-uuid> | jq '{uuid, slug, extractorSlug, kind, connectorUuid}'

# 2. Confirm the extractor's mode is "ingest" (and NOT autoIngest — see below)
cargo-ai connection integration get http | jq -c '.integration.extractors.listenHook.mode'
# → {"kind":"ingest"}

# 3. Pick or create a token (the raw value is on the list response)
cargo-ai workspaceManagement token list | jq -r '.tokens[0].token'
cargo-ai workspaceManagement token create --name "Webhook — <model-slug>" | jq -r '.token.token'
```

One-liner that assembles it:

```bash
MODEL_UUID=<model-uuid>
BASE=$(cargo-ai whoami | jq -r '.baseUrl')
TOKEN=$(cargo-ai workspaceManagement token list | jq -r '.tokens[0].token')
echo "$BASE/v1/models/$MODEL_UUID/records/ingest?token=$TOKEN"
```

> Token values are secrets. Print the URL for the user to copy; don't write it
> into a file, a commit, or a report.

## Skip models where Cargo owns the hook

Some ingest extractors set `autoIngest: true` — Cargo registers the webhook with
the provider itself during setup (calendly, smartlead, instantlyV2, heyReach,
and cargo's own signal extractors). The app **hides** the URL for
those, and handing it out is wrong: the provider is already pointed at it.

Check before showing anything:

```bash
cargo-ai connection integration get <integration-slug> \
  | jq -c '.integration.extractors["<extractor-slug>"].mode'
# {"kind":"ingest"}                    → manual: show the URL
# {"kind":"ingest","autoIngest":true}  → Cargo owns it: don't show the URL
# anything else (fetch/…)              → not an ingest model at all
```

List every ingest extractor an integration has:

```bash
cargo-ai connection integration get <integration-slug> \
  | jq -c '.integration.extractors | to_entries
           | map(select(.value.mode.kind=="ingest")) | map({(.key): .value.mode})'
# calendly → [{"fetchEvents":{"kind":"ingest","autoIngest":true}}]
```

## POST records

The body is **either one flat object or an array of flat objects** — each object
becomes one record, and its keys become columns. Max **100 records per request**.

```bash
# one record
curl -X POST "$BASE/v1/models/$MODEL_UUID/records/ingest?token=$TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","company":"example.com"}'

# many records
curl -X POST "$BASE/v1/models/$MODEL_UUID/records/ingest?token=$TOKEN" \
  -H "Content-Type: application/json" \
  -d '[{"email":"[email protected]"},{"email":"[email protected]"}]'

# token as a header instead of a query param
curl -X POST "$BASE/v1/models/$MODEL_UUID/records/ingest" \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic $TOKEN" \
  -d '{"email":"[email protected]"}'
# all → 200 {"message":"OK"}
```

**The endpoint is insert-only.** Do not send an envelope like
`{"kind":"insert","records":[…]}` — there is no unwrapping, so you get one useless
row with a `kind` column and a `records` column holding the stringified array.
The `{kind: insert|update|remove, records: […]}` shape belongs to the extractor's
internal contract, not to this HTTP body; `update` and `remove` are not reachable
through the webhook.

For `http.listenHook`, the model's id column is `_ingest_id` (a UUID **generated
server-side** — never send it; the extractor rejects inserts that carry one) and
its title column is `_emitted_at`.

Then confirm the rows landed (storage queries are free):

```bash
cargo-ai storage query execute "SELECT * FROM <dataset-slug>.<model-slug> LIMIT 10"
```

## Create an ingest model from scratch

`model create` has no `--connector-uuid` — the API infers the connector from the
**dataset**. Every connector automatically owns exactly one `kind: "connector"`
dataset, so the flow is: create the connector, find its dataset, create the model
in it.

```bash
# 1. Connector (slug must be snake_case: /^[a-z0-9]+(_[a-z0-9]+)*$/)
cargo-ai connection connector create \
  --name "Inbound leads" --slug inbound_leads \
  --integration-slug http --config '{}' | jq -r '.connector.uuid'

# 2. Its dataset — dataset list takes no --connector-uuid filter, so filter locally
cargo-ai storage dataset list \
  | jq -c --arg c <connector-uuid> '.datasets[] | select(.connectorUuid==$c) | {uuid, slug}'

# 3. The model
cargo-ai storage model create \
  --slug inbound_leads --name "Inbound Leads" \
  --dataset-uuid <dataset-uuid> \
  --extractor-slug listenHook --config '{}'
```

The response comes back with `kind: "connector"`, `idColumnSlug: "_ingest_id"`,
and `titleColumnSlug: "_emitted_at"`. Columns are then created dynamically from
the keys of whatever you POST — you don't declare them up front. Query it as
`<connector-slug>.<model-slug>`.

## Notes

- The endpoint answers webhook **handshakes** out of the box — Slack
  `url_verification`, generic `ping`, Microsoft `?validationToken=`, Salesforce
  SOAP ack, and Meta's `hub.challenge` on `GET` — so most providers validate
  without extra work.
- Ingest models can't be refreshed or scheduled; data only arrives when something
  POSTs. `model refresh` / `--schedule` don't apply.
- A legacy alias `POST /v1/workflows/<uuid>/hook` still resolves to the same
  insert (the uuid being the model uuid). Prefer the `/v1/models/...` form.
- Because the URL is assembled client-side, it is *derived*, not *returned* — if
  a future CLI release adds a field or a `get-webhook-url` command, prefer that.
references/examples/models.md›
# Model examples

## Discover all models

```bash
cargo-ai storage model list
```

Response includes `uuid`, `name`, `slug`, `datasetUuid`, and `columns[]` for each model.

## Find a model by name

```bash
# List all models and filter by name in the output
cargo-ai storage model list
# → Find the entry where "name" matches what you're looking for, then extract "uuid"
```

## Get a model's full schema

```bash
cargo-ai storage model get <model-uuid>
# → Returns the model with all columns, their types and slugs
```

## Get the DDL (column types and SQL dialect)

`storage query execute` accepts `<datasetSlug>.<modelSlug>` (e.g. `default.companies`) as the table name, so you don't need the DDL just for the table name. Run `model get-ddl` when you need column types or the SQL dialect.

```bash
cargo-ai storage model get-ddl <model-uuid>
```

Example response:
```json
{
  "ddl": "CREATE TABLE `datasets_default.models_companies` (\n  `uuid` STRING,\n  `name` STRING,\n  `domain` STRING,\n  `employee_count` INT64\n)",
  "language": "bigquery"
}
```

The `language` field tells you which SQL dialect to use.

## Create a model

```bash
# First, find the dataset UUID
cargo-ai storage dataset list

# Create the model
cargo-ai storage model create \
  --slug prospects \
  --name "Prospects" \
  --dataset-uuid <dataset-uuid> \
  --extractor-slug <extractor-slug> \
  --config '{}'
```

## Update a model

```bash
cargo-ai storage model update --uuid <model-uuid> --name "Qualified Prospects"
```

## Remove a model

```bash
cargo-ai storage model remove <model-uuid>
```

Note: This will fail if the model is referenced by segments, plays, or tools. Remove or update those resources first.

## Schema discovery workflow

Full flow to understand a model before querying it:

```bash
# 1. Find the model and its dataset slug
cargo-ai storage model list
cargo-ai storage dataset list

# 2. Get the full schema with column types (optional — also returns SQL dialect)
cargo-ai storage model get <model-uuid>
cargo-ai storage model get-ddl <model-uuid>

# 3. Query using <datasetSlug>.<modelSlug> as the table name
cargo-ai storage query execute \
  "SELECT uuid, name, domain FROM default.companies LIMIT 10"
```
references/examples/queries.md›
# Storage query examples

Run SQL against workspace storage with `cargo-ai storage query execute`. Tables are referenced as `<datasetSlug>.<modelSlug>` and rewritten to the underlying storage table under the hood. No DDL lookup is required for the table name — just use the dataset and model slugs.

For column slugs, run `cargo-ai storage column list --model-uuid <uuid>` or `cargo-ai storage model get-ddl <model-uuid>` (the DDL also shows column types and the SQL dialect).

## Basic query flow

```bash
# 1. Discover the dataset slug and the model slug
cargo-ai storage dataset list   # → datasets[].slug (e.g. "default")
cargo-ai storage model list     # → models[].slug   (e.g. "companies")

# 2. Query using <datasetSlug>.<modelSlug> as the table name
cargo-ai storage query execute \
  "SELECT name, domain, employee_count FROM default.companies LIMIT 10"
```

Success response:

```json
{
  "rows": [
    { "name": "Acme Corp", "domain": "acme.com", "employee_count": 500 },
    { "name": "Globex", "domain": "globex.com", "employee_count": 1200 }
  ]
}
```

Failed commands exit non-zero with `{"errorMessage": "..."}` (or `{"reason": "clientNotFound"|"unknown"}`). See the error handling section below.

## Query with WHERE clauses

```bash
# Filter by a column
cargo-ai storage query execute \
  "SELECT name, domain FROM default.companies WHERE employee_count > 100"

# Multiple conditions
cargo-ai storage query execute \
  "SELECT name, domain, revenue FROM default.companies WHERE employee_count > 100 AND country = 'US'"

# LIKE for partial matches
cargo-ai storage query execute \
  "SELECT name, domain FROM default.companies WHERE name LIKE '%tech%'"

# NULL checks
cargo-ai storage query execute \
  "SELECT name, domain FROM default.companies WHERE email IS NOT NULL"
```

## Aggregation queries

```bash
# Count records
cargo-ai storage query execute \
  "SELECT COUNT(*) as total FROM default.companies"

# Group by with counts
cargo-ai storage query execute \
  "SELECT country, COUNT(*) as count FROM default.companies GROUP BY country ORDER BY count DESC"

# Sum and average
cargo-ai storage query execute \
  "SELECT country, SUM(revenue) as total_revenue, AVG(employee_count) as avg_employees FROM default.companies GROUP BY country"
```

## Pagination

Page through large result sets with SQL `LIMIT` and `OFFSET` clauses. Always include an `ORDER BY` so pages are stable across calls.

```bash
# First page
cargo-ai storage query execute \
  "SELECT * FROM default.companies ORDER BY name LIMIT 100 OFFSET 0"

# Second page
cargo-ai storage query execute \
  "SELECT * FROM default.companies ORDER BY name LIMIT 100 OFFSET 100"
```

## Download full results

For exporting full result sets to a file, use `storage query download`. The response is a signed URL.

```bash
cargo-ai storage query download \
  --query "SELECT name, domain, employee_count, revenue FROM default.companies ORDER BY revenue DESC"

# Choose the format (csv default, parquet supported)
cargo-ai storage query download \
  --query "SELECT * FROM default.companies" --format parquet
```

## Query across multiple models

Join on `<datasetSlug>.<modelSlug>` table references:

```bash
cargo-ai storage query execute \
  "SELECT c.name, c.domain, d.stage, d.amount FROM default.companies c JOIN default.deals d ON c._id = d.company_id WHERE d.amount > 10000"
```

## Common table expressions

```bash
cargo-ai storage query execute \
  "WITH recent AS (SELECT * FROM default.companies WHERE created_at >= CURRENT_DATE - INTERVAL '30' DAY) SELECT count(*) FROM recent"
```

## Date queries

```bash
# Records created in the last 30 days
cargo-ai storage query execute \
  "SELECT name, created_at FROM default.companies WHERE created_at >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)"

# Records in a specific range
cargo-ai storage query execute \
  "SELECT name, created_at FROM default.companies WHERE created_at BETWEEN '2025-01-01' AND '2025-03-31'"
```

## Subqueries

```bash
# Companies with above-average employee count
cargo-ai storage query execute \
  "SELECT name, employee_count FROM default.companies WHERE employee_count > (SELECT AVG(employee_count) FROM default.companies)"
```

## Error handling

If a query fails, the command exits non-zero. Failure shapes:

```json
{ "errorMessage": "Table not found: default.nonexistent" }
```

```json
{ "reason": "clientNotFound" }
```

Common causes:
- Wrong dataset or model slug → re-check with `storage dataset list` and `storage model list`
- Syntax error → check SQL syntax for your storage SQL dialect (BigQuery vs Snowflake) — `storage model get-ddl` reports `language`
- `clientNotFound` → no storage client is configured for this workspace

## Discovery commands

```bash
cargo-ai storage dataset list                  # all datasets (uuid, slug)
cargo-ai storage model list                    # all models (uuid, name, slug)
cargo-ai storage model get-ddl <model-uuid>    # column types and SQL dialect
cargo-ai storage column list --model-uuid <uuid>  # column slugs for a model
```
references/response-shapes.md›
# Response shapes

JSON response structures returned by Cargo CLI commands used in the `cargo-storage` skill.

## cargo-ai storage model list

```json
{
  "models": [
    {
      "uuid": "model-uuid",
      "workspaceUuid": "...",
      "slug": "companies",
      "name": "Companies",
      "datasetUuid": "dataset-uuid",
      "extractorSlug": "hubspot_companies",
      "idColumnSlug": "uuid",
      "titleColumnSlug": "name",
      "timeColumnSlug": null,
      "columns": [
        { "slug": "name", "type": "string", "label": "Name", "kind": "original", "originalSlug": "name" },
        { "slug": "domain", "type": "string", "label": "Domain", "kind": "original", "originalSlug": "domain" }
      ],
      "additionalColumns": [
        { "slug": "full_name", "type": "string", "label": "Full Name", "kind": "computed", "expression": { "kind": "jsExpression", "expression": "..." }, "columnsUsed": ["first_name", "last_name"] },
        { "slug": "total_deals", "type": "number", "label": "Total Deals", "kind": "metric", "relationshipUuid": "...", "aggregation": { "function": "count", "columnSlug": "uuid" } }
      ],
      "unification": null,
      "playsCount": 2,
      "segmentsCount": 1,
      "isPaused": false,
      "lastRun": {
        "uuid": "run-uuid",
        "status": "success",
        "errorMessage": null,
        "createdAt": "2025-01-15T00:00:00Z",
        "finishedAt": "2025-01-15T00:01:00Z"
      },
      "createdAt": "2025-01-01T00:00:00Z",
      "updatedAt": "2025-01-15T00:00:00Z"
    }
  ]
}
```

**Key fields:** `uuid`, `slug`, `name`, `datasetUuid`, `idColumnSlug`, `columns` (original columns), `additionalColumns` (custom/computed/metric/lookup columns).

`unification` is `null` unless the model unifies. When set it is either
`{"source":"integration"}` or the `custom` shape (`type`, `uniqueColumns`,
optionally `selectedColumnSlugs` / `timeColumnSlug` / `parent` / `filter`) —
see the Unification section of `SKILL.md`.

Columns have no `uuid` — they are identified by `slug` within the model.

## cargo-ai storage model get

Same structure as a single item from `model list`, nested under `model`:

```json
{
  "model": {
    "uuid": "model-uuid",
    "slug": "companies",
    "name": "Companies",
    "datasetUuid": "dataset-uuid",
    "columns": [...],
    "additionalColumns": [...]
  }
}
```

## cargo-ai storage model get-ddl

```json
{
  "ddl": "CREATE TABLE `datasets_default.models_companies` (\n  `uuid` STRING,\n  `name` STRING,\n  `domain` STRING,\n  `employee_count` INT64,\n  `created_at` TIMESTAMP\n)",
  "language": "bigquery"
}
```

**Key fields:** `ddl` (contains the storage-native table name and column names), `language` (SQL dialect).

For `cargo-ai storage query execute`, reference tables as `<datasetSlug>.<modelSlug>` (e.g. `default.companies`).

## cargo-ai storage dataset list

```json
{
  "datasets": [
    {
      "uuid": "dataset-uuid",
      "slug": "default",
      "workspaceUuid": "...",
      "config": { "kind": "object" },
      "createdAt": "2025-01-01T00:00:00Z"
    }
  ]
}
```

## cargo-ai storage dataset get

```json
{
  "dataset": {
    "uuid": "dataset-uuid",
    "slug": "default",
    "workspaceUuid": "...",
    "config": { "kind": "object" }
  }
}
```

## cargo-ai storage column list

Returns the model's columns (both original and additional). All columns share base fields: `slug`, `type`, `label`, `kind`. Columns have no `uuid` — use `slug` to identify them.

```json
{
  "columns": [
    {
      "slug": "name",
      "type": "string",
      "label": "Name",
      "kind": "original",
      "originalSlug": "name"
    },
    {
      "slug": "full_name",
      "type": "string",
      "label": "Full Name",
      "kind": "computed",
      "expression": { "kind": "jsExpression", "expression": "..." },
      "columnsUsed": ["first_name", "last_name"]
    }
  ]
}
```

Kind-specific fields are included alongside the base fields:

**`computed`**
```json
{
  "kind": "computed",
  "expression": { "kind": "jsExpression", "value": "record.first_name + \" \" + record.last_name" },
  "columnsUsed": ["first_name", "last_name"]
}
```

**`metric`**
```json
{
  "kind": "metric",
  "relationshipUuid": "relationship-uuid",
  "aggregation": {
    "function": "count",
    "columnSlug": "uuid"
  },
  "filter": null
}
```

**`lookup`**
```json
{
  "kind": "lookup",
  "join": {
    "toModelUuid": "company-model-uuid",
    "fromColumnSlug": "company_uuid",
    "toColumnSlug": "uuid"
  },
  "extractColumnSlug": "name",
  "filter": null
}
```

## cargo-ai storage relationship list

```json
{
  "relationships": [
    {
      "uuid": "relationship-uuid",
      "workspaceUuid": "workspace-uuid",
      "fromDatasetUuid": "dataset-uuid",
      "fromModelUuid": "contacts-model-uuid",
      "fromColumnSlug": "account_id",
      "fromPropertySlug": "hubspot___contacts[0]",
      "toDatasetUuid": "dataset-uuid",
      "toModelUuid": "companies-model-uuid",
      "toColumnSlug": "id",
      "relation": "manyToOne"
    }
  ]
}
```

Workspace-wide — the command takes no flags. `fromDatasetUuid` always equals
`toDatasetUuid`. `fromPropertySlug` / `toPropertySlug` appear only where the
relationship keys off a nested property of a connector column.

`relationship set` returns the same shape, holding the dataset's full set after
the replace.

## cargo-ai storage record list

```json
{
  "records": [
    {
      "uuid": "record-uuid",
      "name": "Acme Corp",
      "domain": "acme.com",
      "employee_count": 500
    }
  ]
}
```

## cargo-ai storage query execute

Tables are referenced as `<datasetSlug>.<modelSlug>` and rewritten to the underlying storage table under the hood.

**Success:**

```json
{
  "rows": [
    { "name": "Acme Corp", "domain": "acme.com", "employee_count": 500 },
    { "name": "Globex", "domain": "globex.com", "employee_count": 1200 }
  ]
}
```

**Failure (non-zero exit):**

```json
{ "errorMessage": "Table not found: default.nonexistent" }
```

```json
{ "reason": "clientNotFound" }
```

```json
{ "reason": "unknown" }
```

## cargo-ai storage query download

Used for full exports. Same table-naming convention as `storage query execute` (`<datasetSlug>.<modelSlug>`). Pass the SQL via `--query`; the response is a signed URL.

**Success:**

```json
{
  "url": "https://signed-url-to-csv-or-parquet-file"
}
```

**Failure (non-zero exit):**

```json
{ "errorMessage": "Table not found: default.nonexistent" }
```
references/troubleshooting.md›
# Troubleshooting

Common errors and recovery steps for `cargo-storage` commands.

## General

| Symptom | Cause | Fix |
|---------|-------|-----|
| `{"errorMessage": "..."}` with non-zero exit | Any CLI error | Read the `errorMessage` — it usually says exactly what's wrong |
| `command not found: cargo-ai` | CLI not installed or not in PATH | Run `npm install -g @cargo-ai/cli` or prefix with `npx @cargo-ai/cli` |
| `Unauthorized` or `Forbidden` | Bad or expired credentials | Re-run `cargo-ai login --oauth` (browser sign-in) or `cargo-ai login --token <token>`; verify with `cargo-ai whoami` |

## Models

| Symptom | Cause | Fix |
|---------|-------|-----|
| `model get` returns not found | Wrong UUID | Re-run `model list` to get the correct UUID |
| `model get-ddl` returns empty DDL | Model has no sync connection to storage | Confirm the model has an extractor configured and has synced at least once |
| Table not found in `storage query execute` | Wrong dataset or model slug | Verify with `dataset list` and `model list`; tables are referenced as `<datasetSlug>.<modelSlug>` |
| `model remove` returns an error | Model is referenced by segments, plays, or tools | Remove or update the dependent resources before deleting the model |

## Columns

| Symptom | Cause | Fix |
|---------|-------|-----|
| `column create` fails with slug conflict | A column with that slug already exists | Use `column list --model-uuid <uuid>` to check existing slugs; choose a unique slug |
| `column update` returns not found | Wrong column slug or model UUID | Re-run `column list --model-uuid <uuid>` to get the correct column slugs |
| Column type mismatch in queries | Using string operators on a number column | Match the condition type to the column type; see the `cargo-orchestration` skill's `references/filter-syntax.md` |

## Relationships

| Symptom | Cause | Fix |
|---------|-------|-----|
| Relationships disappeared after a `set` | `relationship set` replaces the dataset's whole set — anything whose `uuid` is missing from the payload is deleted | `relationship list` first, then send the full array back with your addition, keeping each existing `uuid` |
| `invalidRelationships` | A model UUID or column slug doesn't resolve, or the payload duplicates a pair (including stated in reverse) | Verify UUIDs with `model list` and slugs with `column list --model-uuid <uuid>` |
| `modelNotCompatible` | A unify model was named as `fromModelUuid` or `toModelUuid` | Unify-model relationships are generated during sync and can't be authored by hand |
| `datasetNotFound` | `--dataset-uuid` is wrong, or the two models live in different datasets | Relationships never span datasets; confirm with `model list` → `datasetUuid` |
| `relationship list` returns everything | It takes no flags and is workspace-wide by design | Filter client-side on `fromModelUuid` / `toModelUuid` |

## Unification

| Symptom | Cause | Fix |
|---------|-------|-----|
| `model update --unification` succeeded but nothing merged | Writing the config doesn't recompute; unified rows are rebuilt by the sync run | `storage run create --model-uuid <uuid>`, then poll `storage run list --model-uuid <uuid>` |
| Duplicates survive unification | `uniqueColumns` is too narrow, or the match key is dirty (mixed case, `www.` prefixes) | Widen or normalize the key, then re-run |
| Distinct entities merged into one | `uniqueColumns` is too broad (e.g. matching on a shared generic domain) | Add a second key column, or scope with `filter` |
| Contacts not attached to accounts | `parent` is unset on a `contact` unification | Set `parent` to the account model and the joining column slug |

## Records

| Symptom | Cause | Fix |
|---------|-------|-----|
| `record list` returns empty | No records in the model, or wrong model UUID | Verify with `model list`; check that data has been synced |
| Need filtered record access | `record list` doesn't support filtering | Use `segmentation segment fetch` from the `cargo-orchestration` skill for filtering, sorting, and pagination |

## Queries (`storage query execute` / `storage query download`)

| Symptom | Cause | Fix |
|---------|-------|-----|
| `errorMessage` with "Table not found" | Wrong dataset or model slug | Verify with `storage dataset list` and `storage model list`. Tables are `<datasetSlug>.<modelSlug>` |
| `errorMessage` with syntax error | SQL dialect mismatch | Check whether your storage backend is BigQuery, Snowflake, etc. and adjust syntax accordingly. `storage model get-ddl` reports `language` |
| `reason: "clientNotFound"` | No storage client configured | Verify the workspace has an active storage connection |
| Query returns empty `rows` | Filter too restrictive, or wrong model | Try a broader query first (`SELECT * FROM <dataset>.<model> LIMIT 5`) |
| Column not found | Wrong column slug | Run `storage column list --model-uuid <uuid>` to get exact slugs |
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-storage",
  "version": "1.2.1",
  "documents": [
    {
      "path": "SKILL.md",
      "kind": "entrypoint",
      "title": "Cargo CLI — Storage"
    },
    {
      "path": "references/examples/columns.md",
      "kind": "example",
      "title": "Column examples"
    },
    {
      "path": "references/examples/datasets.md",
      "kind": "example",
      "title": "Dataset examples"
    },
    {
      "path": "references/examples/ingest-webhook.md",
      "kind": "example",
      "title": "Ingest models — get the webhook URL and POST records"
    },
    {
      "path": "references/examples/models.md",
      "kind": "example",
      "title": "Model examples"
    },
    {
      "path": "references/examples/queries.md",
      "kind": "example",
      "title": "Storage query examples"
    },
    {
      "path": "references/response-shapes.md",
      "kind": "reference",
      "title": "Response shapes"
    },
    {
      "path": "references/troubleshooting.md",
      "kind": "reference",
      "title": "Troubleshooting"
    }
  ],
  "contentHash": "1ae84fb240b7a9f0af53ad57c9e33587567284fcb9c568d8edbfefd87353ccce"
}
SKILL.md›
---
name: cargo-storage
description: "Work with the data inside a Cargo workspace — models (Companies, Contacts, Deals…), datasets, columns, relationships, records, and SQL over workspace storage. Triggers: \"what models do I have\", \"show me the schema\", \"add a column for\", \"how many contacts do I have\", \"SELECT … FROM\", \"query my companies table\", \"join contacts to companies\", \"what is the DDL\", \"set up a webhook-fed model\", \"where does this field live\", \"import this into a model\", \"unify these models\", \"merge duplicate accounts\", \"link contacts to companies\", \"set up a relationship between\". Skip when: querying run or batch telemetry rather than business data — use cargo-orchestration; naming a reusable filtered audience — use cargo-segmentation."
version: "1.2.1"
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 CLI — Storage

Data layer management: inspecting and modifying models, datasets, columns, relationships, unification, and records, and running SQL queries against workspace storage.

> See `references/response-shapes.md` for full JSON response structures.
> See `references/troubleshooting.md` for common errors and how to fix them.
> See `references/examples/models.md` for model CRUD, DDL inspection, and schema discovery examples.
> See `references/examples/datasets.md` for dataset listing and navigation examples.
> See `references/examples/columns.md` for column creation and management examples.
> See `references/examples/queries.md` for `storage query execute` / `storage query download` SQL examples (WHERE, aggregations, joins, pagination, exports).
> See `references/examples/ingest-webhook.md` for ingest (webhook-fed) models — deriving the webhook URL and POSTing records.

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

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.

## Discover resources first

Always list before inspecting or modifying.

```bash
cargo-ai storage dataset list              # all datasets (uuid, slug)
cargo-ai storage model list                # all models (uuid, name, slug, columns)
cargo-ai storage model list --dataset-uuid <uuid>   # models in a specific dataset
```

**Retrieve in the UI:** models live at `app.getcargo.io/workspaces/<WORKSPACE_UUID>/models/<MODEL_UUID>`. Get `<WORKSPACE_UUID>` from `cargo-ai whoami` under `workspace.uuid`.

## Quick reference

```bash
cargo-ai storage model list
cargo-ai storage model get <model-uuid>
cargo-ai storage model get-ddl <model-uuid>
cargo-ai storage dataset list
cargo-ai storage column list --model-uuid <uuid>
cargo-ai storage relationship list
cargo-ai storage record list --model-uuid <uuid>
cargo-ai storage query execute "SELECT * FROM default.companies LIMIT 10"
cargo-ai storage query download --query "SELECT * FROM default.companies"
```

## Models

Models are structured tables in your workspace (e.g. Companies, Contacts).

```bash
# List all models
cargo-ai storage model list

# List models in a dataset
cargo-ai storage model list --dataset-uuid <uuid>

# Get a single model (includes columns)
cargo-ai storage model get <model-uuid>

# Get the DDL (full schema, table name and SQL dialect)
cargo-ai storage model get-ddl <model-uuid>
# → Useful for column discovery and SQL dialect (BigQuery vs Snowflake) before writing queries

# Create a model
cargo-ai storage model create \
  --slug contacts \
  --name "Contacts" \
  --dataset-uuid <uuid> \
  --extractor-slug <extractor-slug> \
  --config '{}'

# Update a model
cargo-ai storage model update --uuid <model-uuid> --name "New Name"

# Remove a model
cargo-ai storage model remove <model-uuid>
```

**Querying:** Use `cargo-ai storage query execute "<sql>"` (or `storage query download --query "<sql>"` for full exports) to run SQL against storage. Tables are referenced as `<datasetSlug>.<modelSlug>` (e.g. `default.companies`) and rewritten to the underlying storage table under the hood. See [Query with SQL](#query-with-sql) below.

## Ingest models (webhook-fed)

A model whose extractor has `mode.kind === "ingest"` — `http.listenHook` and
friends — is filled by **pushing** records to Cargo. The app shows a "Webhook URL"
on the model settings screen; **no CLI command or API field returns it**, but it's
assembled from values the CLI already exposes:

```
<baseUrl>/v1/models/<model-uuid>/records/ingest?token=<api-token>
```

```bash
MODEL_UUID=<model-uuid>
BASE=$(cargo-ai whoami | jq -r '.baseUrl')
TOKEN=$(cargo-ai workspaceManagement token list | jq -r '.tokens[0].token')
echo "$BASE/v1/models/$MODEL_UUID/records/ingest?token=$TOKEN"
```

Check the extractor's mode first — when it reports `"autoIngest": true` (calendly,
smartlead, instantlyV2, heyReach, cargo signals) Cargo registers the
hook with the provider itself and the URL must **not** be handed out. Full flow,
payload shapes, and limits: `references/examples/ingest-webhook.md`.

## Datasets

Datasets are logical groupings of models.

```bash
# List all datasets
cargo-ai storage dataset list

# Get a single dataset
cargo-ai storage dataset get <dataset-uuid>
```

## Columns

Columns define the schema of a model.

```bash
# List columns for a model
cargo-ai storage column list --model-uuid <uuid>

# Create a column
cargo-ai storage column create \
  --model-uuid <uuid> \
  --column '{"slug":"my_column","type":"string","label":"My Column","kind":"custom"}'

# Update a column (pass the full column object — columns are identified by slug, not UUID)
cargo-ai storage column update \
  --model-uuid <uuid> \
  --column '{"slug":"my_column","type":"string","label":"Updated Label","kind":"custom"}'

# Remove a column
cargo-ai storage column remove --model-uuid <uuid> --column-slug <slug>

# Reorder a column (move to a specific index)
cargo-ai storage column reorder --model-uuid <uuid> --column-slug <slug> --to-index 2
```

Column types: `string`, `number`, `boolean`, `date`, `object`, `array`, `vector`, `any`.

Column kinds: `custom` (user-defined), `computed` (expression over other columns), `metric` (aggregated from a related model), `lookup` (single field pulled from a related model via a join).

## Preview what you built

A column list doesn't tell the user whether the model is right — rows do. Two checkpoints (the pack-wide convention lives in [`../cargo/references/interaction.md`](../cargo/references/interaction.md) §4):

**1. Right after `model create` / `column create` — show the schema, not rows.** A new model is empty; a `LIMIT 10` here returns nothing and reads as failure. Echo the columns as a compact table instead (column, type, what will fill it).

**2. As soon as data lands — show the rows.** After a batch, play, or import writes into the model, preview it:

```bash
cargo-ai storage query execute \
  "SELECT * FROM <dataset-slug>.<model-slug> LIMIT 10"
```

Show ~10 rows and only the columns that carry meaning. Storage queries are free, so this costs nothing but a few lines of output — and it's the first moment the user can actually see what they built. When a play fills a *new* column, preview that column next to the record's identifying fields (`name`, `domain`) so filled vs. empty is obvious.

If the preview comes back empty or all-null when it shouldn't, that's a finding — surface it rather than reporting the write as a success. See [`cargo-diagnostics`](../cargo-diagnostics/SKILL.md) to trace why.

## Relationships

Relationships link models together (e.g. Contacts belong to Companies). They are
authored from the CLI, not just the UI.

`relationship list` takes **no flags** — it returns every relationship in the
workspace. Filter client-side on `fromModelUuid` / `toModelUuid`.

```bash
cargo-ai storage relationship list
```

**`relationship set` replaces the dataset's whole relationship set.** It takes a
dataset and the complete list that should exist within it: entries carrying a
`uuid` are updated, entries without one are created, and **any existing
relationship whose `uuid` is absent from the payload is deleted**. Sending one
relationship to a dataset that has five removes the other four. Always `list`
first, then send back the full array with your addition:

```bash
cargo-ai storage relationship set \
  --dataset-uuid <dataset-uuid> \
  --relationships '[
    {"uuid":"<existing-uuid>","fromModelUuid":"<contacts-uuid>","fromColumnSlug":"account_id","toModelUuid":"<companies-uuid>","toColumnSlug":"id","relation":"manyToOne"},
    {"fromModelUuid":"<deals-uuid>","fromColumnSlug":"company_id","toModelUuid":"<companies-uuid>","toColumnSlug":"id","relation":"manyToOne"}
  ]'
```

`relation` is `oneToOne`, `manyToOne`, or `oneToMany`. Both models must live in
the dataset you pass — relationships never span datasets, so `fromDatasetUuid`
and `toDatasetUuid` on the response always equal `--dataset-uuid`.

Failure reasons: `datasetNotFound`; `invalidRelationships` (a column slug or
model UUID that doesn't resolve, or a duplicate — including the same pair stated
in reverse); `modelNotCompatible` (see below).

**Unify models refuse manual relationships.** In the native dataset, a unify
model's relationships are generated during sync, so naming one as `fromModelUuid`
or `toModelUuid` returns `modelNotCompatible`. Those auto-generated rows are also
excluded from the replace above, so a `set` call cannot delete them.

## Unification

Unification is what merges records from several source models into one canonical
account/contact — and it is **configurable from the CLI**, via `--unification` on
`model update`. Pass `null` to clear it.

```bash
# Connector-driven: the integration decides how records unify
cargo-ai storage model update --uuid <model-uuid> --unification '{"source":"integration"}'

# Custom: you name the type, the matching keys, and optionally a parent
cargo-ai storage model update --uuid <model-uuid> --unification '{
  "source": "custom",
  "type": "account",
  "uniqueColumns": [{"slug":"domain","reference":"domain"}],
  "selectedColumnSlugs": ["name","industry","employee_count"],
  "parent": {"kind":"model","columnSlug":"account_id","parentModelUuid":"<accounts-uuid>"}
}'
```

| Field | Applies to | Meaning |
|---|---|---|
| `source` | both | `integration` (connector-defined) or `custom` |
| `type` | custom | `account`, `contact`, `accountEvent`, `contactEvent` |
| `uniqueColumns` | custom | Match keys — `{slug, reference}` per column. This is what decides which rows are the same entity |
| `selectedColumnSlugs` | custom | Columns carried into the unified model. Omit for all |
| `timeColumnSlug` | custom | Event timestamp — for the two `*Event` types |
| `parent` | custom | Links contacts/events to their account: `{"kind":"model","columnSlug":…,"parentModelUuid":…}` or `{"kind":"reference","columnSlug":…,"reference":…}` |
| `filter` | custom | Segmentation filter restricting which rows unify — same `conjonction` shape as segments |

**Writing the config does not recompute anything.** The unified rows are rebuilt
by the model's sync run, so follow the update with a run and poll it:

```bash
cargo-ai storage run create --model-uuid <model-uuid>
cargo-ai storage run list --model-uuid <model-uuid>
```

Get the current config from `storage model get <uuid>` → `unification` (`null`
when the model doesn't unify). Once the run finishes, check the row count with
`storage query execute` before treating the change as done — a too-narrow
`uniqueColumns` under-merges and a too-broad one collapses distinct entities, and
both look like a successful run.

## Records

```bash
# List records in a model
cargo-ai storage record list --model-uuid <uuid>
```

For advanced record queries (filtering, sorting, pagination), use `segmentation segment fetch` from the `cargo-orchestration` skill.

## Query with SQL

Run SQL against workspace storage with `storage query execute`. Tables are referenced as `<datasetSlug>.<modelSlug>` (e.g. `default.companies`) and rewritten to the underlying storage table under the hood — no DDL lookup is needed for the table name.

```bash
cargo-ai storage query execute \
  "SELECT name, domain FROM default.companies LIMIT 10"
# → { "rows": [...] } on success; non-zero exit with { "errorMessage": "..." } on error
```

For full exports, use `storage query download` — it returns a signed URL to a CSV (default) or Parquet file:

```bash
cargo-ai storage query download \
  --query "SELECT name, domain, revenue FROM default.companies ORDER BY revenue DESC"

cargo-ai storage query download \
  --query "SELECT * FROM default.companies" --format parquet
```

Get column slugs from `storage column list --model-uuid <uuid>` (or run `storage model get-ddl <model-uuid>` for the full schema and SQL dialect). Page through large result sets with `LIMIT` / `OFFSET` directly in the SQL.

See `references/examples/queries.md` for WHERE clauses, aggregations, joins, date queries, pagination, and the failure shapes returned on error.

## Help

Every command supports `--help`:

```bash
cargo-ai storage model list --help
cargo-ai storage column create --help
cargo-ai storage relationship set --help
cargo-ai storage query execute --help
cargo-ai storage query download --help
```