> ## Documentation Index
> Fetch the complete documentation index at: https://trysalty.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent guide

> How an AI agent should operate a Salty workspace. Same content the MCP server returns as `instructions` on initialize.

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](/docs/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_*`** — by design, but `DELETE` is a recoverable soft-delete to a 30-day trash; restore via `POST /trash/<resource>/<id>/restore` (resource ∈ people, companies, deals, notes, tasks, activities, custom-objects) or the `/records/trash` admin UI.

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:

| Object                | Required fields                                                          | Notable optional                                                                                                                                           | MCP write?         |
| --------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| `person`              | — (all optional)                                                         | `email`, `first_name`, `last_name`, `primary_company_id`, `custom_attributes`                                                                              | ✓                  |
| `company`             | `name`                                                                   | `domain`, `custom_attributes`                                                                                                                              | ✓                  |
| `deal`                | `name`                                                                   | `value_cents` (**string-encoded bigint**), `currency`, `stage`, `primary_company_id`, **`primary_person_id`**. Response also: `closed_at` (currently null) | ✓                  |
| `note`                | `parent_object_type`, `parent_object_id`, `body`                         | —                                                                                                                                                          | ✓ (`add_note`)     |
| `task`                | `parent_object_type`, `parent_object_id`, `title`                        | `due_at`, `status` (defaults to `open`), `completed_at`                                                                                                    | ✗ REST/CLI         |
| `activity`            | `parent_object_type`, `parent_object_id`, `activity_type`, `occurred_at` | `payload` (free-form JSON)                                                                                                                                 | ✓ (`log_activity`) |
| custom object records | depends 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.** There's no MCP `delete_*` tool, but nothing an agent does is irreversible: every `DELETE` is a recoverable soft-delete to a 30-day trash (auto-purged after 30 days). If the user explicitly asks to delete, tell them to use `curl -X DELETE` (REST, e.g. `DELETE /people/{id}`) or the `/records` admin UI. To undo, `POST /trash/<resource>/<id>/restore` (resource ∈ people, companies, deals, notes, tasks, activities, custom-objects) or use the `/records/trash` 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.** Webhooks are registered over REST/CLI, not MCP (there is no webhook tool). Surface the `whsec_…` to the user immediately on the `POST /webhook-endpoints` (REST) or `salty webhooks add` (CLI) 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`.
9. **Your key may be read-only.** If a write returns `403 insufficient_scope`, you were issued a read-only API key (`scopes: ["read"]`) — you can read but not create/update/delete. Tell the user; they can create a full-access key from the dashboard.

## Common workflows

### Create a person at a company that may not exist yet

```text theme={null}
search_companies   {filter: {domain: {equals: "<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: {equals: <id>}}}` — if exactly 1 result, surface them. If multiple, ask the user to designate.

### Log a meeting

```json theme={null}
{
  "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

```json theme={null}
{
  "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:

```bash theme={null}
# 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](/docs/webhooks/introduction).

## Errors and what to do

| HTTP                           | code                                                                                                     | what to do |
| ------------------------------ | -------------------------------------------------------------------------------------------------------- | ---------- |
| 400 `validation_failed`        | Read `error.param`; don't retry blindly.                                                                 |            |
| 400 `parent_not_found`         | Verify with `search_*` first.                                                                            |            |
| 400 `attribute_enum_invalid`   | Pick a valid value or append a new one via PATCH (REST/CLI only).                                        |            |
| 401                            | Tell the user to reconnect.                                                                              |            |
| 403 `no_workspace`             | Tell the user to refresh `/api`.                                                                         |            |
| 403 `insufficient_scope`       | Read-only API key attempted a write — tell the user their key is read-only; they need a full-access key. |            |
| 409 `idempotency_in_flight`    | Wait briefly, retry once.                                                                                |            |
| 422 `idempotency_key_mismatch` | Pick a NEW key — don't reuse.                                                                            |            |
| 429 `rate_limit_exceeded`      | Respect `Retry-After`.                                                                                   |            |
| 429 `cap_exceeded`             | Tell 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)
