getcargohq/cargo-skills包含需要注意的行為
SKILL DETAIL
cargo-storage
getcargohq/cargo-skills/cargo-storage
The cargo-storage skill works with the data inside a Cargo workspace. It allows you to inspect and manage models (such as Companies, Contacts, Deals), datasets, columns, relationships, records, and run SQL queries against workspace storage. This skill supports listing and getting models, viewing DDL, creating and updating models, managing columns (including create, update, remove, and reorder), listing and setting relationships, configuring unification, and executing SQL queries. It also supports ingest models that receive data via webhooks.
安裝量 · 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
```