Skip to main content
This page is the canonical guidance an agent needs to operate a Salty workspace without trial-and-error reconnaissance. It’s mirrored verbatim in three places:
  1. MCP server-level instructions — returned on initialize to every MCP client (Claude Desktop, Cursor, etc.); the client injects it into the model’s system prompt automatically. Source: apps/mcp/src/instructions.ts.
  2. AGENTS.md at the repo root — convention for non-MCP agent frameworks (Codex, OpenAI Agents SDK, Claude Code).
  3. This docs page — for humans reading the docs to understand the agent contract.

You are operating a Salty workspace — a developer-first CRM exposed over REST and MCP. Use the 15 MCP tools (see MCP introduction) to read and write the workspace’s data on the user’s behalf.

Your 15 tools (this is the complete list)

search_people · get_person · create_person · update_person · search_companies · get_company · create_company · update_company · search_deals · create_deal · update_deal · add_note · log_activity · get_schema · add_attribute. Deliberately not in MCP (the user drives these via REST or the salty CLI; you guide them with snippets):
  • No task tools — use POST /tasks REST or salty CLI.
  • No webhook tools — use POST /webhook-endpoints REST or salty webhooks add.
  • No custom-object tools — use POST /custom-objects REST.
  • No get_deal — use search_deals with a filter.
  • No get_usage / get_workspace — admin UI / CLI only.
  • No delete_* — REST / CLI only, by design.
Keep the tool surface tight — more tools = larger context window pollution = worse agent performance.

30-second mental model

Salty has six native object types plus user-defined custom objects:
ObjectRequired fieldsNotable optionalMCP write?
person— (all optional)email, first_name, last_name, primary_company_id, custom_attributes
companynamedomain, custom_attributes
dealnamevalue_cents (string-encoded bigint), currency, stage, primary_company_id, primary_person_id. Response also: closed_at (currently null)
noteparent_object_type, parent_object_id, body✓ (add_note)
taskparent_object_type, parent_object_id, titledue_at, completed✗ REST/CLI
activityparent_object_type, parent_object_id, activity_type, occurred_atpayload (free-form JSON)✓ (log_activity)
custom object recordsdepends on add_attribute✗ REST only
Every object has a stable UUID id, ISO-8601 created_at/updated_at, and (for native objects) a free-form custom_attributes JSON blob validated lazily by the schema engine. All operations are scoped to the workspace owning your access token. You never pass a workspace_id — it’s implicit.

Rules that aren’t obvious from tool descriptions

  1. No delete tools, by design. Deletion routes through the REST API where a human can review. If the user explicitly asks to delete, tell them to use salty people revoke <id> (CLI), curl -X DELETE (REST), or the /records admin UI.
  2. Money is a string-encoded bigint. deal.value_cents is JSON-string (e.g. "5000000" = $50,000.00). Currency defaults to USD; pass currency: "INR" etc. if otherwise. deal.closed_at is response-only (currently always null; v1.1 auto-sets on stage→won/lost — don’t write it).
  3. Parent references use parent_object_type + parent_object_id. Notes, tasks, and activities attach to a parent via these two fields. Valid parent_object_type: person, company, deal.
  4. Schema attributes need 4 fields minimum. add_attribute requires attribute_key (snake_case), display_name (Title Case), data_type, and object_type. Enum types need enum_values. Reference types need reference_object_type.
  5. custom_attributes validation is LENIENT. Defined keys are validated; unknown keys pass through. Required attrs ARE enforced.
  6. Webhooks return signing_secret ONCE. Surface the whsec_… to the user immediately on register_webhook response.
  7. subscribed_events: ["*"] is the default. No glob patterns in v1.
  8. Pagination is cursor-based. Pass next_cursor back as the cursor param. No offset/page.

Common workflows

Create a person at a company that may not exist yet

search_companies   {filter: {domain: "<derived from email>"}}
  ↓ if 0 results:
create_company     {name: "<inferred>", domain: "<derived>"}
  ↓ save id
create_person      {email, first_name, last_name, primary_company_id: <id>}
One write per object — don’t create the person first then PATCH the link.

Who’s the primary contact at a company?

There’s no companies.primary_contact_id field in v1 (v1.1 candidate). Resolve:
  1. Look for custom_attributes.primary_contact_id on the company — that’s the convention.
  2. If unset, search_people {filter: {primary_company_id: <id>}} — if exactly 1 result, surface them. If multiple, ask the user to designate.

Log a meeting

{
  "tool": "log_activity",
  "arguments": {
    "parent_object_type": "person",
    "parent_object_id": "<id>",
    "activity_type": "meeting",
    "occurred_at": "2026-05-25T14:00:00Z",
    "payload": { "topic": "...", "duration_min": 30, "attendees": [] }
  }
}

Add a tier field to people

{
  "tool": "add_attribute",
  "arguments": {
    "object_type": "person",
    "attribute_key": "tier",
    "display_name": "Tier",
    "data_type": "enum",
    "enum_values": ["free", "pro", "enterprise"]
  }
}
Writes can immediately use custom_attributes: {tier: "pro"} after this returns 201.

”Create a task” / “Register a webhook” / “Define a custom object” — REST-only

These have no MCP tools. Guide the user with snippets:
# task
curl -X POST $SALTY_API/tasks \
  -H "Authorization: Bearer $SALTY_API_KEY" -H "Content-Type: application/json" \
  -d '{"parent_object_type":"person","parent_object_id":"<id>","title":"...","due_at":"2026-05-29T17:00:00Z"}'

# webhook (signing_secret in response is whsec_… shown ONCE)
curl -X POST $SALTY_API/webhook-endpoints \
  -H "Authorization: Bearer $SALTY_API_KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://...","subscribed_events":["deal.created"]}'
Or via the salty CLI (salty webhooks add --url …). The webhook receiver must verify Salty-Signature: t=<ms>,v1=<hex> (HMAC-SHA-256 over <t>.<body>) and reject deliveries older than 5 minutes. See Webhooks.

Errors and what to do

HTTPcodewhat to do
400 validation_failedRead error.param; don’t retry blindly.
400 parent_not_foundVerify with search_* first.
400 enum_value_not_allowedPick a valid value or append a new one via PATCH (REST/CLI only).
401Tell the user to reconnect.
403 no_workspaceTell the user to refresh /api.
409 idempotency_in_flightWait briefly, retry once.
422 idempotency_key_mismatchPick a NEW key — don’t reuse.
429 rate_limit_exceededRespect Retry-After.
429 cap_exceededTell the user to upgrade plan or raise hard_cap at /pricing.

Out of scope for v1

  • Bulk import endpoints (v1.1)
  • Search/filter on custom-object records (v1.1)
  • Webhook event glob patterns (v1.1)
  • Updating an activity’s parent (immutable)
  • Company.primary_contact_id field (v1.1 candidate)