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
`storage model list` takes **no options** — it always returns every model in the
workspace. Each one carries a `datasetUuid`, so narrow it client-side:
```bash
cargo-ai storage model list | jq '[.models[] | select(.datasetUuid == "<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. Group the models by dataset (one call — `model list` has no filter flag)
cargo-ai storage model list | jq 'group_by(.datasetUuid) | map({datasetUuid: .[0].datasetUuid, models: map(.slug)})'
# → 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":"ada@example.com","company":"example.com"}'
# many records
curl -X POST "$BASE/v1/models/$MODEL_UUID/records/ingest?token=$TOKEN" \
-H "Content-Type: application/json" \
-d '[{"email":"ada@example.com"},{"email":"grace@example.com"}]'
# 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":"ada@example.com"}'
# 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.2",
"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": "e71fc21bf17d68ad87d94f3df61e13e0c2b4e0e0272fc24b9a730850681a32a7"
}
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.2"
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 you@company.com # 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, datasetUuid)
# `model list` takes no flags — filter its output instead:
cargo-ai storage model list | jq '[.models[] | select(.datasetUuid == "<uuid>")]'
```
**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 — every model carries `datasetUuid`, and
# `model list` has no flags of its own, so filter client-side
cargo-ai storage model list | jq '[.models[] | select(.datasetUuid == "<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
```