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

# CRM

> Contacts, organizations, deals, campaigns. Call describe_schema before you guess a field key.

URL: `https://mcp.chatsailer.com/mcp/crm`

Read and manage a Sailer workspace. Record tools take a `resource` of
`"contact"`, `"organization"`, or `"deal"` so they can cover the CRM without
eating the client's tool budget. Campaigns and conversation traces have their
own tools. There is no send on a live conversation.

A tool that spans resources is listed if you have **any** of its read or write
scopes, then checks the scope for the resource you named. A `deals:read`
connection sees `search_records` but cannot search contacts.

Every workspace defines its own custom fields. The JSON Schema a client caches
does **not** include them — clients cache by server, not by tenant. That is
why `describe_schema` exists, and why you call it first.

<Note>
  First connection is [read-only](/mcp/auth). Write tools will not even appear
  until you reconnect with the matching write scope — `contacts:write`,
  `organizations:write`, `deals:write`, or `campaigns:write`.
</Note>

## Orient

### `whoami`

No arguments, no scope. Which workspace this connection is bound to, who
authorized it, `oauth` vs `api_token`, whether it reads company-wide, and the
scopes you actually have.

> Which Sailer workspace am I connected to?

A Sailer account can belong to several workspaces. The connection is bound to
exactly one, chosen at consent. No tool argument can change it.

### `describe_schema`

Needs any of `contacts:read`, `organizations:read`, or `deals:read`. Optional
`resource` (`"contact"`, `"organization"`, `"deal"`); omit it to describe
everything this connection can read.

Returns built-in and custom fields, which are required or read-only, select
options, and which keys are filterable, sortable, or expandable. On deals it
also returns the workspace's pipelines and stages — you need a valid pair to
create a deal.

> What custom fields does this workspace have?

Use the `key` it returns inside `custom_fields` on create and update. Guessing
from another workspace — or from these docs — is how you get an unknown-field
error.

## Read records

Ids look like `con_8f3a…`, `org_8f3a…`, `deal_8f3a…`. The prefix **is** the
resource. `get_record` and `update_record` take no `resource` argument — a pair
that disagreed would have no principled winner.

### `search_records`

Needs any of `contacts:read`, `organizations:read`, or `deals:read`. `resource`
defaults to `"contact"`. The named resource's own read scope is what authorizes
the call.

| Argument   | What it does                                                       |
| ---------- | ------------------------------------------------------------------ |
| `resource` | `"contact"`, `"organization"`, or `"deal"`                         |
| `q`        | Free text across name, email, phone                                |
| `filter`   | Nested and/or condition tree                                       |
| `sort`     | Same syntax as the REST `sort` query                               |
| `expand`   | Comma-separated relations, e.g. `owner`                            |
| `limit`    | Page size                                                          |
| `cursor`   | Opaque. Pass `next_cursor` from the previous page back as `cursor` |

If `has_more` is true, keep paging. Do not count a page and call it the
population — that is what `count_records` is for. Cursors are opaque, same
contract as [REST pagination](/guides/pagination).

> Find contacts created in the last 7 days.

> Find deals in Negotiation.

### `get_record`

Needs any of `contacts:read`, `organizations:read`, or `deals:read`. `id`,
optional `expand`. The prefix on `id` selects the resource.

> Show me contact `con_…`

> Show me deal `deal_…`

Missing, deleted, or outside what you can see: not found. Same next step in
all three cases — search again.

### `count_records`

Needs any of `contacts:read`, `organizations:read`, or `deals:read`. Same
`resource` / `filter` / `q` as search, no paging.

> How many contacts are in this workspace?

> How many organizations are in this workspace?

## Write records

Call `describe_schema` first. Unknown or read-only custom-field keys are
rejected, not ignored.

Required on create depends on `resource`:

| `resource`       | Required                                                                                   |
| ---------------- | ------------------------------------------------------------------------------------------ |
| `"contact"`      | `first_name`, `phone` (unique per workspace, stored as E.164)                              |
| `"organization"` | `name`                                                                                     |
| `"deal"`         | `title`, `pipeline_id`, `stage_id` — and at least one of `contact_id` or `organization_id` |

### `create_record`

Needs any of `contacts:write`, `organizations:write`, or `deals:write`. New
row. Prefer `upsert_contact` if the person may already exist — a duplicate is
not automatically reversible.

> Create a contact named Ana with phone +5511999999999.

> Create an organization named Acme.

### `update_record`

Needs any of `contacts:write`, `organizations:write`, or `deals:write`. `id`
only — no `resource` argument. Destructive: it overwrites what you send. Omit
a field to leave it; send `null` to clear it. Read first if you meant to
append.

> Set Ana's email to [ana@example.com](mailto:ana@example.com).

### `upsert_contact`

Scope: `contacts:write`. Contacts only. Match on phone, after
canonicalization — `(11) 98765-4321` and `+55 11 98765-4321` are the same
person. Default `on_match` is `update`; `ignore` returns the existing row
untouched.

The result tells you which branch ran (`created` vs `matched_existing`). The
record alone does not.

> Add or update the contact with this phone number.

## Deals

### `move_deal_stage`

Scope: `deals:write`. `deal_id`, plus `stage_name` or `stage_id`.

Give `stage_name` — the name as it appears on the board, matched
case-insensitively — and it is resolved **within this deal's own pipeline**.
You cannot accidentally move a deal into another pipeline's stage this way.
Use `stage_id` only if you already have one.

> Move Acme to Negotiation.

## Campaigns

### `list_campaigns`

Scope: `campaigns:read`. Newest first. Optional `status`, `limit` 1–100
(default 25). Use this to get a `campaign_id`.

> List this workspace's campaigns.

### `get_campaign`

Scope: `campaigns:read`. One campaign plus a page of its participants.
Optional `participant_limit` (default 25; `0` for the campaign alone).
`projected_send_at` is an estimate, not a commitment.

For aggregate performance, call `get_campaign_analytics` instead — this
returns rows, not totals.

> Who is in this campaign, and where does each contact stand?

### `get_campaign_analytics`

Scope: `campaigns:read`. One campaign, one metric family.

| `metric`          | What you get                        |
| ----------------- | ----------------------------------- |
| `big_numbers`     | Headline counts and rates (default) |
| `status_funnel`   | Lifecycle funnel                    |
| `daily_metrics`   | Per-day series                      |
| `error_breakdown` | Where sends failed                  |

Optional `window_start` / `window_end` (UTC). Defaults to the last 30 days.

These families are **not interchangeable**. `big_numbers` and `daily_metrics`
use a strict "answered" (a qualifying inbound, classified as a reply).
`status_funnel` uses the lifecycle status, which is usually a larger number.
Rates on `big_numbers` are 0–100, not fractions.

Every response carries `_meta.caveats`. Read them before comparing two
numbers. A difference between two families is not a change over time.

> Show me campaign performance. Read the caveats before you summarize.

### `add_campaign_participants`

Scope: `campaigns:write`. **Destructive.** Sends real messages to real people.
Confirm with the user before calling.

Enrolling a contact in a running campaign means the campaign will message them
on its own schedule — a WhatsApp message to an actual phone number. That
cannot be recalled once sent.

The contacts must already exist; this does not create them. Use
`search_records` or `upsert_contact` first. Anyone already in the campaign is
reported in `already_present` and is not enrolled twice. There is no way to
make a campaign send immediately from here.

> Add these contacts to the campaign. Confirm with me before you call it.

## Conversations

There is no tool that lists conversations or sends a message on one. REST can
list and retrieve them; a model reaches a conversation from the contact it is
already working with. Sending on a live conversation is unwired on purpose —
use the [Studio sandbox](/mcp/studio) if you need to test a turn.

### `get_conversation_trace`

Scopes: `conversations:read` and `inference:read`. `conversation_id`, optional
`since`.

Explains why the agent routed a real conversation the way it did: every
routing edge it considered, which conditions passed, and the values they
compared. The useful part is usually the edges that *lost*. Pass `since` to
scope this to one turn — without it, a long thread returns a great deal of
history.

This does not include prompts, model completions, model names, token counts,
or cost.

> Why did the agent say that to this customer?
