Claude Cursor Skill

cargo-storage

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 … FRO

LLM Mart · 0 points · 19 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download getcargohq-cargo-skills-cargo-storage-bb59f8d.zip · 18 KB
Part of getcargohq/cargo-skills — 22 skills

Install

skills CLI npx skills add https://github.com/getcargohq/cargo-skills/tree/main/cargo-storage
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install getcargohq-cargo-skills@llmmart
Git git clone https://github.com/getcargohq/cargo-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole getcargohq/cargo-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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.

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 adds the CLI version pin, token scopes, and the admin-only surface.

Discover resources first

Always list before inspecting or modifying.

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

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).

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

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

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

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 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.

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:

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.

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

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

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

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:

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:

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
Files (cargo-skills)
  • references
    • examples
      • columns.md 5.3 KB
        # 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.
        
      • datasets.md 1.2 KB
        # 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`).
        
      • ingest-webhook.md 6.7 KB
        # 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.
        
      • models.md 2.2 KB
        # 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"
        ```
        
      • queries.md 4.9 KB
        # 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
        ```
        
    • response-shapes.md 6.3 KB
      # 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" }
      ```
      
    • troubleshooting.md 4.8 KB
      # 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 1.3 KB
    {
      "$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 14.3 KB
    ---
    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
    ```
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related