# Tool reference

Source: https://docs.havilo.ai/mcp/tools

The workspace MCP catalog contains 40 tools. Enumerate the MCP `tools/list` response before choosing a tool. All data, import, AI, and job operations are constrained to the workspace stored on the OAuth grant or API key; none accepts `workspace_id`. An API key reaches only the 13 read and Engine tools listed in [Connect with an API key](https://docs.havilo.ai/mcp/api-keys.md#what-a-key-can-reach).

## Shared conventions

* CRM, import, and AI mutation tools default to `apply: false` and return a preview. Situation mutations act immediately after authorization; they do not accept `apply`.
* `apply: true` crosses the write or enqueue boundary.
* Idempotency keys are 8–256 characters and may contain letters, numbers, `_`, `.`, `:`, and `-`.
* Entity, field, entity-type, relationship-type, Note, and job IDs are strings with a maximum length of 191 characters.
* Times supplied for relationships use ISO 8601 date-time strings.
* Read limits are bounded by the server even when a client requests more.

Permissions are evaluated when the tool is called. The catalog itself does not shrink when permissions change.

## Annotations

Every tool carries a `title` and standard MCP annotations describing its possible effects. Clients use these hints when deciding how to present and confirm a call; confirmation behavior depends on the client and its settings.

* `ai_understand` and `ai_plan` have `readOnlyHint: true`: they interpret or propose work without executing it or changing CRM records. Internal usage accounting and reviewed-plan persistence do not require approval. Table, email draft and document tools remain writes because they create or edit user-facing artifacts.
* `destructiveHint: true` marks tools that can overwrite or absorb existing CRM data or drafts: `crm_entity_types_ensure`, `crm_fields_ensure`, `crm_relationship_types_ensure`, `crm_relationships_ensure`, `crm_records_upsert`, `crm_notes_edit`, `crm_notes_upsert`, `crm_records_merge`, `crm_drafts_edit`, `situations_draft_revise`, and `imports_csv`. Preview defaults do not remove this annotation because `apply: true` can overwrite data. Field changes can also convert stored values, and CSV imports can update matched records.
* `idempotentHint: true` holds for every mutation that takes an idempotency key or ensures a named object. Producing a table, draft, or model call is not idempotent. `crm_fields_suggest` returns an identical open suggestion rather than duplicating it, but declares `destructiveHint: true` because a different proposal can supersede the current suggestion. The previous row remains as history.
* `openWorldHint: false` applies to private imports, table artifacts, drafts, understanding, and planning. `ai_run` and `ai_runs_start` retain `true` because their reviewed plans can consult public sources and external discovery services. This does not imply that they send email.

The catalog is 21 read-only tools and 19 write tools.

External `crm_fields_suggest` calls remain attributed to the authenticated user. The `work_job_id` parameter is reserved for Havilo's internal agent; OAuth clients cannot use it to attribute their suggestions to Situation Work.

## Results and failures

Read tools return structured JSON containing the requested workspace objects and pagination metadata where applicable. Mutation previews return `applied: false`, the resolved target, and the proposed plan. Applied mutations return `applied: true`, whether anything changed, and the resulting object or durable job handle.

For external OAuth clients, artifact query, draft creation/editing, document writing, and artifact reads include `artifact_url` wherever they return an `artifact_id`, including each entry in `drafts`. The URL uses the authenticated resource's origin and `/app/<workspace_id>/artifacts/<artifact_id>`. Render it as an ordinary HTTPS link. Server initialization also supplies the workspace's browser routes for notes and Situations. Havilo's in-app client continues using `[name](artifact=id)`; that internal reference is not a browser link in ChatGPT or Claude.

A tool call fails before writing when its input is invalid, a required permission is missing, an ID belongs to another workspace, or an idempotency key has already been used with different input. Relationship external IDs may identify only one exact relationship; reusing one for different endpoints, type, qualifier, or workspace returns an error.

## Connection and capabilities

### `workspace_whoami`

Returns the authenticated user, OAuth client and grant, environment, resource, bound workspace, and membership. It has no input and requires an active workspace grant.

### `workspace_capabilities`

Returns the OAuth scope and the effective permissions currently implied by the connected membership's role and permission sets. It has no input and requires an active workspace grant.

## Schema

### `crm_schema_get`

Returns active entity types, fields, relationship types, and Entity counts.

* **Input:** optional `entity_type_id`.
* **Permission:** `ENTITY_READ`.

Each field carries `records_with_value`: how many of the type's records have it filled in, to read against `record_count` beside it. A field can be defined and hold nothing, and querying an empty field returns rows anyway, so read this before writing a query against a field you have not used before. Counts are taken on a six-hour TTL; `field_values_counted_at` says when.

Pass `entity_type_id` to return one type instead of the whole model. That is usually what a write needs, and on a large workspace it is a fraction of the payload.

### `crm_entity_types_ensure`

Previews or idempotently creates or updates one entity type.

* **Input:** `name`, `singular_name`, `icon`, optional `description` and `color`, and optional `apply`.
* **Preview permission:** `ENTITY_READ`.
* **Apply permissions:** `ENTITY_CREATE` and `ENTITY_UPDATE`.

### `crm_fields_ensure`

Previews or idempotently creates or updates one field on an existing entity type.

* **Input:** `entity_type_id`, `name`, `type`, `icon`, optional `field_id`, `description`, `required`, `is_list`, `options`, `split_existing_values_on`, `join_existing_values_with`, and `apply`.
* **Preview permission:** `ENTITY_READ`.
* **Apply permissions:** `ENTITY_CREATE` and `ENTITY_UPDATE`.

`type` accepts `TEXT`, `LONG_TEXT`, `NUMBER`, `DATE`, `TIME`, `DATETIME`, `BOOLEAN`, `EMAIL`, `PHONE`, `URL`, `PICKLIST`, or `MULTI_PICKLIST`. `options` is used for list-backed fields and accepts at most 500 values.

`field_id` targets an existing field. Supply it to rename a field in place; omit it and the field is matched on `name`, so a rename would create a second field instead of changing the first.

`is_list` (multi-value storage) is only supported for `TEXT`, `EMAIL`, `PICKLIST`, and `MULTI_PICKLIST`. `MULTI_PICKLIST` is always list storage and rejects `is_list: false`. Flipping `is_list` on a field that already has values moves them between storage tables in the same transaction as the flag change; the preview response's `list_storage_migration` reports the row counts and any blocker before you apply.

`split_existing_values_on` and `join_existing_values_with` decide what happens to those stored values. Turning `is_list` on splits each stored value on the delimiter you name; turning it off joins each record's values with the separator you name, and without one the change is refused for any record holding more than one value rather than silently discarding the rest.

`icon` is required for new MCP field definitions and must be an exact Phosphor icon name from `ENTITY_ICON_NAMES`. The server still resolves omitted or legacy-null icons for older internal callers, but MCP clients should always supply the icon explicitly.

### `crm_relationship_types_ensure`

Previews or idempotently creates or updates one relationship type.

* **Input:** `name`, `from_type_id`, `to_type_id`, optional `description`, `forward_label`, `reverse_label`, `bidirectional`, and `apply`.
* **Preview permission:** `ENTITY_READ`.
* **Apply permissions:** `ENTITY_CREATE` and `ENTITY_UPDATE`.

## Entities and relationships

### `crm_records_search`

Searches or pages through active Entities.

* **Input:** optional `entity_type_id`, `query`, `external_id`, `page`, and `limit`.
* **Permission:** `ENTITY_READ`.
* **Limits:** `limit` is 1–100 and defaults to 50. Ranked text search is limited to the first 1,000 results; narrow `query` or `entity_type_id` instead of requesting a deeper window.
* **Text matching:** ranked indexed search. Name fields match prefixes and the start of later words. Other populated scalar and list fields match exact values and prefixes. It does not perform arbitrary mid-token substring search or search Notes, relationships, mail, or artifacts.
* **Pagination:** text search returns `has_more`, `search_window_limit: 1000`, and `total_count: null` because exact-counting the full match population would repeat the expensive scan. Pagination without `query` and exact `external_id` lookup return an exact numeric `total_count`.
* **Output:** records carry `entity_type_id`, populated `fields`, `entity_source`, and `import_hash`. The response does not echo Entity type or field definitions; call `crm_schema_get` when you need the schema.

> Changed 2026-08-14: text search no longer performs an unindexed arbitrary substring scan or an
> exact count. It now ranks indexed field matches and searches normalized list values as well as
> scalar values.

### `crm_records_get`

Returns one active Entity with its fields, relationships, and recent Notes.

* **Input:** `record_id`.
* **Permission:** `ENTITY_READ`.
* **No match:** returns `{ "record": null }` when the id is missing, inactive, or belongs to another workspace.

The Entity must belong to the connected workspace.

Each Note carries a `role` saying how the Entity points at it: `primary` for the Note shown first on the record, `dossier` for a generated briefing, and `linked` for the rest. A Note reachable more than one way is listed once, under the most specific role.

When the caller has `KNOWLEDGE_READ`, the result also carries up to 50 linked knowledge items with
their IDs, titles, summaries, source IDs, and current processing state. It does not return file text;
use `knowledge_item_read` for that.

### `crm_records_upsert`

Previews or upserts as many as 100 Entities.

* **Input:** `records` and optional `apply`.
* **Existing Entity:** provide `record_id`, `entity_type_id`, and a `fields` object keyed by field ID.
* **New Entity:** provide `entity_type_id`, `fields`, and at least one of `external_id` or `idempotency_key`.
* **Preview permission:** `ENTITY_READ`.
* **Apply permission when every item has `record_id`:** `ENTITY_UPDATE`, or `ENTITY_UPDATE_LINKED` when every record is linked to the caller.
* **Apply permissions when any item may be created:** `ENTITY_CREATE` and `ENTITY_UPDATE`.

Use `record_id` for a known Havilo Entity. Use a stable external ID or idempotency key when the source owns the identity.

### `crm_fields_suggest`

Files field values for one Entity as suggestions a person accepts or rejects. Nothing is written to the record; each suggestion waits under its field on the record page until someone decides.

* **Input:** `record_id`, `fields` keyed by field name or ID, and `reasoning` (what was said or read, one sentence, shown to whoever decides).
* **Permission:** `ENTITY_READ`. Deciding the suggestion requires `ENTITY_UPDATE` or `ENTITY_UPDATE_LINKED`.
* **Result per field:** `pending` (filed, or an identical open suggestion already stood), `unchanged` (the record already holds the value), or `declined` (a person already rejected that exact value; do not suggest it again).

Single-value fields only. A picklist accepts only its own options.

### `crm_relationships_ensure`

Previews or idempotently ensures as many as 100 typed relationships.

* **Input:** `relationships` and optional `apply`.
* **Relationship input:** `relationship_type_id`, one reference per end, and optional `qualifier`, `external_id`, `title`, `start_date`, `end_date`, and `metadata`.
* **Preview permission:** `ENTITY_READ`.
* **Apply permissions:** `ENTITY_CREATE` and `ENTITY_UPDATE`.

Name each end either by `from_record_id` / `to_record_id`, or by the key it was imported under: `from_external_id` with `from_entity_type_id`, and `to_external_id` with `to_entity_type_id`. External ID is unique per workspace and entity type, so the pair resolves exactly — which means an import can link its rows without a lookup pass to turn external IDs into Havilo ids first.

Both Entities and the relationship type must belong to the connected workspace. If supplied, `external_id` must continue to identify these exact endpoints, type, qualifier, and workspace on later calls.

## Notes

### `crm_notes_get`

Reads one active Note's markdown.

* **Input:** `note_id`.
* **Permission:** `NOTE_READ`.

Note ids come from an Entity's `notes` in `crm_records_get`, which lists the 50 most recently updated as `{id, title, summary, updated_at}`. Discovery happens on the record, reading happens here — a workspace can hold hundreds of thousands of Notes, so there is no tool that pages through all of them.

### `crm_notes_search`

Finds Notes by name or topic, ranked, the way `crm_records_search` finds Entities.

* **Input:** `query`, optional `source_type`, optional `limit` (1–50, default 10).
* **Permission:** `NOTE_READ`.
* **Matching:** any query term in the title, or a body containing all of them. Title matches rank above body matches, and an exact phrase in the title ranks highest.
* **Limits:** the candidate pool is capped at 50 before ranking. When it fills, the response says `truncated: true` — narrow the query rather than paging, because there is no deeper window.
* **Output:** `id`, `title`, `summary`, `source_type`, `updated_at`, and `editable`.

Both columns are matched through their FULLTEXT indexes, which hold no token shorter than three characters. A term below that is dropped, and a query with nothing longer left in it — `Q3`, `AI` — comes back with no notes and an `unsearchable` sentence instead. That is the whole answer: there is no slower path behind it to fall through to, because the only statement that could find such a term reads every note the workspace can reach. Commission the Engine for it, which reads note content rather than matching it.

Use it whenever you need a Note and do not already have its id. It reaches Notes attached to no Entity, which `crm_records_get` cannot, and it is always cheaper than opening Entities one at a time to see what is on them. An empty result means no Note matches that name.

### `crm_notes_read`

Reads one active Note as complete markdown, with the revision handle `crm_notes_edit` requires.

* **Input:** `note_id`.
* **Permission:** `NOTE_READ`.

Custom blocks appear as `:::` fences — `:::callout`, `:::person-card`, `:::keypoints`, `:::entity-hero`, `:::signal-strip`, `:::team-grid`, `:::timeline`, `:::section-label`, `:::numbered-insights`, `:::footnote`, `:::quote`, `:::columns`, `:::divider-fade`. They are ordinary text: edit inside one, replace a whole one, or write a new one.

The markdown is read from the stored document rather than from a cached column, so it is always current. `revision` names that exact text. `editable` is `false` for a Note this workspace can read but does not own — an enrichment Note linked in from the Global workspace — and for a Note another system writes: a dossier, an enrichment document, a Data Bridge import, a calendar-event note, or a template-controlled Note. `crm_notes_edit` and `crm_notes_upsert` refuse both, and the error names which.

### `crm_notes_edit`

Applies exact-string edits to one Note, leaving everything else untouched.

* **Input:** `note_id`, optional `revision`, `edits`, optional `append`, and optional `apply`.
* **Preview permission:** `NOTE_READ`.
* **Apply permission:** `NOTE_UPDATE`.
* **Limits:** at most 50 edits per call.

Each edit is `{old_string, new_string, replace_all}`. `old_string` must appear **exactly once** unless `replace_all` is set; otherwise the call fails rather than choosing an occurrence. Edits apply in order, so a later edit sees what an earlier one wrote. `append` adds markdown to the end and needs no anchor, which is the right tool for "record that they mentioned X".

Call `crm_notes_read` first and copy `old_string` from what it returned. Pass its `revision` back: if the Note changed in between, the edit is refused instead of overwriting the change.

A call fails, writing nothing, when:

* `old_string` does not appear — the Note has changed, or the text differs in whitespace or punctuation;
* `old_string` is ambiguous — add surrounding context, or set `replace_all`;
* `revision` is stale — read the Note again and reapply against the current text;
* the Note holds a block that cannot survive a markdown round trip. The error names the block. Use `crm_notes_upsert` to replace the Note deliberately.

> That last check runs on preview as well as apply. Editing through markdown means editing a
> projection of the stored document, so before writing anything the server re-parses the result and
> confirms no kind of content disappeared. Measured over 1,296 live Notes, it clears 99.8% of them.

Writes require the connected workspace to own the Note, so reading a linked enrichment Note never implies editing it.

### `crm_notes_upsert`

Previews, creates, or updates one markdown Note and its Entity links.

* **Existing Note:** provide `note_id` and `markdown`; `title`, `entity_ids`, and `apply` are optional.
* **New Note:** provide `idempotency_key` and `markdown`; `title`, `entity_ids`, and `apply` are optional.
* **Preview permission:** `NOTE_READ`.
* **Apply permission with `note_id`:** `NOTE_UPDATE`.
* **Apply permissions without `note_id`:** `NOTE_CREATE` and `NOTE_UPDATE`.
* **Limits:** markdown is at most 1,000,000 characters and `entity_ids` accepts at most 100 Entities.

The idempotency key makes Note creation safe to retry. Reusing it with different input returns an error.

## Knowledge and product guidance

### `knowledge_items_list`

Lists the files a workspace holds, or the files linked to one known Entity or held by one known knowledge source.

* **Input:** all optional — `record_id`, `source_id`, `title_or_path_contains`, and `limit` (1–100, default 50).
* **Permission:** `KNOWLEDGE_READ`.
* **Effect:** deterministic title and path lookup, most recently modified first. It does not search inside documents.

With no filter it lists the whole workspace. `total` counts everything the filters match and `limit` caps what comes back, so a full page means there are more. Use an item ID from the result with `knowledge_item_read`.

### `knowledge_item_read`

Reads extracted text from one known knowledge item with stable citations.

* **Input:** `item_id`, optional anchor, and optional ordinal range.
* **Permission:** `KNOWLEDGE_READ`.
* **Citations:** page, slide, spreadsheet cell range, heading, or content-block number, depending on the source format.
* **No match:** returns `{ "item": null }` when the item is absent or belongs to another workspace.

This tool reads only the requested item. It does not search document contents.

### `platform_manual_get`

Reads trusted Havilo product and workflow guidance shipped with the server.

* **Input:** optional exact `section_id`.
* **Permission:** `AI_FEATURE_ACCESS`.
* **Without `section_id`:** lists the available sections.

Use this for questions about how Havilo itself should perform a documented workflow. Workspace Notes
are customer content and are not a substitute for the product manual.

## Duplicate review

### `crm_records_merge`

Previews or applies a reviewed duplicate-record decision table.

* **Input:** `artifact_id`, optional `plan_hash`, and optional `apply`.
* **Preview permissions:** `ENTITY_READ` and `AI_FEATURE_ACCESS`.
* **Apply permissions:** `ENTITY_UPDATE`, `BULK_OPERATIONS`, and `AI_FEATURE_ACCESS`.

The artifact must be a complete saved table with `Record A`, `Record B`, `Decision`, and `Reasoning`
columns. `Decision` accepts `INCLUDE`, `EXCLUDE`, or `AMBIGUOUS`. Preview validates every row and
returns a `plan_hash`; apply requires that exact hash. Included pairs use Havilo's reversible Entity
Resolution merge path. Never merge from a name query alone.

## Data and imports

### `databridge_workspaces_list`

Lists active DataBridge connections and mappings already attached to the connected workspace.

* **Input:** none.
* **Permission:** `ENTITY_READ`.
* **Effect:** read-only. It cannot create a bridge, change credentials, or update mappings.

### `imports_inspect`

Read a CSV by the same file reference used for import. Returns normalized headers, full-file row counts, blank/duplicate counts, candidate stable keys and bounded samples. Makes no record changes. In-app attachments can be inspected before selecting an entity type.

### `imports_match`

Answers "who on this list do we already know?" for an attached CSV, without importing anything. The AI Engine's file fetch reads the whole file from storage, works out which columns name people or organizations, matches their values against the workspace's records, and returns a table with every row marked. Matching is by value: exact on email, phone and URL, close-spelling on names. It finds nobody by concept.

* **Input:** `attachment_id`, `file_name`; optional `match_filter` (`all`, `matched`, `unmatched`), `title`, `rows`.
* **Permissions:** `ENTITY_READ` and `AI_FEATURE_ACCESS`.
* **Effect:** reads records and writes one saved table. It creates and updates nothing.
* **Registered only in-app**, alongside attachments — without a file there is nothing to match.
* Read further into the table with [`crm_artifacts_get`](#crm_artifacts_get), or link it for the reader to open.

### `imports_table`

Shows an attached CSV as a saved table, read whole from storage. `where` selects rows exactly as [`imports_csv`](#imports_csv) does; `columns` picks and orders columns. No AI call.

* **Input:** `attachment_id`, `file_name`; optional `where`, `columns`, `title`, `rows`.
* **Permission:** `ENTITY_READ`.
* **Effect:** writes one saved table. It creates and updates no records.
* **Registered only in-app**, alongside attachments.

### `imports_upload_url`

Returns a URL to upload a CSV to, so an external client never sends a file's contents as tool
arguments. PUT the file to `upload_url`, then pass `upload_token` to [`imports_csv`](#imports_csv).

* **Input:** `entity_type_id`, `file_name`.
* **Permissions:** `BULK_OPERATIONS`, `ENTITY_CREATE`, and `ENTITY_UPDATE`.
* **Effect:** creates an upload slot. It changes no records.

### `imports_csv`

Parses and validates a mapped CSV, then optionally enqueues a durable import.

* **Input:** `entity_type_id`, `file_name`, one file source (`attachment_id` in-app, `upload_token` externally, or `csv`), `external_id_column`, `field_mapping`, `idempotency_key`; optional `where`, `relationship_mapping`, `note_columns`, `note_mode`, `list_mode`, `list_delimiter`, and `apply`.
* **Preview permission:** `ENTITY_READ`.
* **Apply permissions:** `BULK_OPERATIONS`, `ENTITY_CREATE`, and `ENTITY_UPDATE`.
* **Limits:** at most 100,000 selected data rows and 25 MiB per file reference; 64 KiB inline.

`list_delimiter` separates values in a column mapped to a multi-value field. It defaults to a comma, but most exports — Getro, LinkedIn, and most ATS tools — use a semicolon, which produces one wrong value per cell instead of several right ones. The preview reports `unmatched_options`: picklist values matching no declared option on the field, which is what a wrong delimiter looks like. Check it before applying.

`field_mapping` maps each source column to a Havilo field ID or exact field name. Field IDs are more stable if a field is later renamed. Preview returns the resolved mapping, headers, row count, sample rows, and payload size. Apply returns the canonical import job; follow it with the job tools.

## AI

### `ai_understand`

Runs only query understanding, without planning or execution. It reads the query against the workspace schema, with how many records have a value in each field.

Its result includes `schema_questions`, a list, empty when there are none. Each names a field the request's population depends on that too few records hold to tell a blank that means "no" from one never recorded, and the question to settle: `{ "field": "Role", "question": "Is Role the curated list of investors, or do most investors have no Role?" }`. Settle each before `ai_plan`, by reading the field's values or asking the user, and say in the `ai_plan` query how to treat the field. `ai_plan` never returns one.

* **Input:** `query`.
* **Permission:** `AI_FEATURE_ACCESS`.
* **Effect:** invokes a model but does not enqueue a durable run.

### `ai_plan`

Understands a query and returns a validated plan without executing it, along with the `plan_hash` that names it, a one- or two-sentence `summary` of what the plan does (on `plan` and `execution_plan`), and `credits_at_most`: the most the plan can cost, in credits. The in-app agent's `ai_run` starts a plan of 50 credits or less at once and holds a dearer one for the person; an MCP client enqueues on `ai_run` either way.

* **Input:** `query`.
* **Permission:** `AI_FEATURE_ACCESS`.
* **Effect:** invokes models and saves the reviewed plan and execution settings, without executing them.

There is deliberately no reach parameter here. How far a run reaches — this workspace, the Global Network, a lifted transform cap — is the user's setting rather than the caller's, so it is fixed by the connection and authorized there. An MCP connection carries the narrowest reach: `ai_plan` always reasons within the connected workspace and needs only `AI_FEATURE_ACCESS`.

### `ai_run`

Starts a run of a plan `ai_plan` already returned, naming it by `plan_hash` rather than restating it.

* **Input:** `plan_hash` and `idempotency_key`.
* **Permission:** `AI_FEATURE_ACCESS`.
* **Effect:** enqueues a durable run and returns a receipt, not an answer.

The receipt carries `applied: true` and `changed: true` by the convention above, and both describe
the ENQUEUE. Beside them it carries `status: "queued"` and `produced: null`, which describe the run:
nothing has executed, and no rows, artifact or findings exist yet. A caller that reads `applied` as
the outcome will report work that has not happened. `credits_at_most` is the bound `ai_plan` fixed when the
plan was reviewed, and is what the run holds against the caller's credits.

The hash is a value `ai_plan` handed you, never one to compose or recall from elsewhere: it is content-addressed, so it cannot name a slightly different plan than the one that was reviewed. Call `ai_plan` first — until it has run there is no plan to start.

The 16-character handle resolves from durable storage for the authenticated user and workspace.
Planning and execution can reach different server instances. Execution rechecks current permissions;
possessing a handle does not grant access. Handles issued before durable storage was deployed need
a new plan unless the complete historical plan was imported.

### `ai_runs_start`

Validates and previews a reviewed plan, then optionally enqueues it as a durable run.

* **Input:** `plan`, `idempotency_key`, optional `chat_mode` and `apply`.
* **Preview permission:** `AI_FEATURE_ACCESS`; `xray` and `global_search` also require `VIEW_GLOBAL_NETWORK`.
* **Apply permission:** the same permissions as preview.
* **Effect with `apply: true`:** creates or reuses the idempotent AI job and returns its handle.

The preview's `plan` carries `credits_at_most`, the most the plan can cost, before anything is enqueued.

Although preview and apply require the same Havilo permission, only apply crosses the enqueue boundary.

## Tables and artifacts

### `crm_records_query`

Queries stored Entity field values into a table using a plain-language request.

* **Input:** `request`, and optional `title`, `rows`, and semantic `limit`.
* **Permissions:** `ENTITY_READ` and `AI_FEATURE_ACCESS`.
* **Effect:** uses a model to write a constrained query against the connected workspace, then returns the statement, row count, columns, up to the requested rows, and the `artifact_id` of the table it wrote. The table is unsaved: it is not in the workspace's Artifacts until [`crm_artifacts_save`](#crm_artifacts_save) saves it.

`rows` controls how many result rows come back to the caller, up to 200. Left out, the whole result
comes back when it is 50 rows or fewer, and the first 10 when it is larger. `limit` belongs in the question itself,
such as the top 50 by deal count. When `is_complete` is false, read the remaining saved rows with
`crm_artifacts_get`; do not describe the returned sample as the complete result.

### `crm_artifacts_save`

Puts tables you made into the workspace's Artifacts, or takes them back out.

* **Input:** `artifact_ids` (1–20), `saved` (`true` or `false`).
* **Permission:** `AI_FEATURE_ACCESS`.
* **Effect:** sets whether each table is saved. Nothing is deleted, and an unsaved table can still be read by id. Refuses the whole call, changing nothing, if any table was not made by `crm_records_query`, `imports_match` or `imports_table` for you in the same conversation.

Read a query's result before saving it. Save it when it is the set that was asked for; query again when it is not.

### `crm_artifacts_get`

Reads a saved table or written report that belongs to the connected workspace.

* **Input:** `artifact_id`, optional `limit` (1–200), optional `offset`, or optional exact `row_indices`.
* **Permission:** `AI_FEATURE_ACCESS`.
* **Effect:** read-only. Ownership is checked against the connected workspace.

Use `row_indices` for a scattered on-screen selection. Use pagination for ordinary table reads.

## Drafting

### `crm_drafts_email`

Has one email written and left as a draft in the sender's mailbox, saved as an `EMAIL_EDITOR` artifact — something the reader opens, edits and sends, rather than prose in a reply. The same writer Situation Work uses for the drafts on its desk.

* **Input:** `request`, `to`, `intent`, and optional `cc`, `from`, `template_note_id`, `title`.
* **Permission:** `ENTITY_READ` **and** `AI_FEATURE_ACCESS`.
* **Effect:** runs one Engine generation step, saves one artifact bound to the sending mailbox, and creates the provider draft in it. It does not send. A caller with no connected mailbox that can hold a draft gets a `NEW` email saved unbound, in Havilo only; a `REPLY` is refused.

`request` is a brief, not the finished email. What you write there is rewritten against the recipients' fields, notes and relationships, so an argument that is already prose only asks for itself back.

`to` is the To line of one email, one entry per person, each either `entity=<uuid>` for a workspace record or an address, optionally with a name (`Dana Reyes <dana@x.com>`). Everyone on it receives the same message; three different emails are three calls. A record with no address on file comes back with a note saying the draft cannot be sent as it stands.

`intent` is required. `{ "kind": "REPLY", "source_message_id": "…" }` names the exact message being answered — a message id from `mail_thread_read` (in the app), or a Situation's `MAIL_MESSAGE:<id>` reference — and the draft threads onto that conversation from the mailbox holding it, keeping its subject; the Engine is shown the conversation so far. `{ "kind": "NEW" }` opens a conversation, with `subject` fixing the subject line or leaving it to the Engine. A refused reply is a choice to make, never something to retry as a new email.

`from` is the caller's connected mailbox the draft is written from, by address. It may be omitted when they hold exactly one or none; with several it is required, and the refusal names them. The draft is signed as the mailbox's owner: there is no separate sender argument, because the mailbox is who the email is from. With no mailbox it is signed by the caller's name and written without their own sent mail or signature, since both are read from the mailbox.

Anything the draft should draw on goes in `request` as an ordinary mention — `[Name](entity=uuid)`, `[Title](note=uuid)` — and arrives hydrated. There is no separate context argument.

Returns `draft` — `artifact_id`, `name`, `revision`, `to`, `cc`, `subject`, `body`, and `replies_to` for a reply — plus `from`, the address it sits in. An unbound draft returns `from: null` and a top-level `note` saying it can be sent once a mailbox with draft access is connected.

> The draft is bound to its mailbox when it is saved, not when it is sent. A reply carries the
> message it answers in that binding, which is what makes it leave as a reply; a draft with no
> binding can only ever leave as a new conversation.

### `crm_documents_write`

Saves a markdown document as a `TEXT_EDITOR` artifact, listed under Documents in Artifacts. The caller writes the document; the tool only saves it, with no AI call. Meant for something the reader will keep or send, such as a memo or a one-pager. A question is answered in the reply.

* **Input:** `title`, `markdown`.
* **Permission:** `ENTITY_READ`.
* **Effect:** writes one saved artifact. It changes no records.

### `crm_drafts_edit`

Changes a draft that already exists, in place, keeping the same artifact id and the same link.

* **Input:** `artifact_id`, optional `revision`, and one or more of `edits`, `append`, `subject`, `name`, `to`, `cc`, `bcc`, `request`.
* **Permission:** `ENTITY_READ` **and** `AI_FEATURE_ACCESS`.
* **Effect:** rewrites the artifact's content. It does not send.

Prefer this over drafting again whenever a draft exists. Drafting again writes a new email from a brief, which changes the subject and rewords sentences nobody asked about, and leaves the old draft saved and orphaned.

Two ways to say what to change, and the exact one is almost always right:

| field     | what it does                                               | cost           |
| --------- | ---------------------------------------------------------- | -------------- |
| `edits`   | names the text to replace; must match exactly once         | no model call  |
| `request` | describes the change in words; rewrites through the Engine | one model call |

`edits` takes `{old_string, new_string, replace_all}` and fails rather than guessing when its anchor is absent or ambiguous — the same contract as [`crm_notes_edit`](#crm_notes_edit). It cannot touch a word it was not pointed at, which is what makes "add my name to the sign-off" safe. `subject`, `name`, `to`, `cc` and `bcc` are set outright and compose with either mode. When both `edits` and `request` are given, the exact edits win and no model runs.

`name` is what the draft is called for the reader, not part of the email. It is reachable because a draft is created named after who it is to — "Email to Taylor Cordoba" — and the recipient is editable: without it, moving a draft to somebody else leaves it filed under a person it is no longer addressed to. It is never inferred from a recipient change, because a name the caller chose and a name the drafting step derived are indistinguishable once stored.

`revision` is the guard. Every call that returns a draft — `crm_drafts_email`, `crm_artifacts_get`, and this tool — hands back the current one; pass it and an edit written against text the reader has since changed in the composer is refused instead of applied over their work.

## Situations

Situations are Havilo's own record of what your workspace is carrying: a real-world matter, kept
current from your mail, calendar, and notes, with what it means and who owes what. These tools read
that record and let you answer, correct, or redirect it.

Every one of them requires the **Intelligence Layer** on the workspace and the **`SITUATION_ACCESS`**
permission for you. That permission is granted per member rather than by default, so a refusal names
which of the two is missing — neither is the same answer as having no matters.

Havilo never sends an email. There is no tool here that could.

### `situations_list`

Lists or searches the matters you can see, filed by who is blocked.

* **Input:** optional `query`, `standing`, `viewer`, `record_id`, `changed_since`, `page`, `limit`.
* **Limits:** `limit` is 1–50 and defaults to 25. Defaults to the `NEEDS_YOU` and `WAITING` standings.

Standings are `NEEDS_YOU` (you control the next step), `WAITING` (someone else owes something),
`READING` (not judged yet), `QUIET`, and `RESOLVED`. Each row carries the sentence that belongs to its
standing, who is awaited, and what Havilo is currently doing on it. Forks are nested under the matter
they came out of. Counts are exact over the whole filtered set, not the page.

`query` matches words in a matter's name. `viewer` names a colleague by email or user id and reports
how these matters stand for them — the matters themselves are still only the ones you can see.
`record_id` narrows to matters that have leaned on one Entity at some revision.

### `situations_get`

Returns one matter as its record.

* **Input:** `situation_id`.

Carries the title and summary, dated `developments` with their citations, `open_items` naming the
party who owes and the party who is owed, `unknowns`, the open `obligations`, related matters, and
`your_attention` — the judgment written for you, including why it is yours and what happens if it
waits. `also_on` reports how the same matter stands for everyone else it reaches, as their standing
and next step.

Evidence appears as coordinates and citations. Message bodies are owner-private and are never
returned; open the email through the mail surface instead. An id that is not a matter you can see
returns `null`, which is a completed lookup rather than a failure.

### `situations_work_read`

Returns what Havilo has prepared on one matter and what it is waiting on.

* **Input:** `situation_id`.

`work_state` is `AWAITING_HUMAN` when it has asked someone a question, `AWAITING_ENGINE` while a
commissioned run is out, `RUNNING_AGENT` while it is working, `FINISHED` when it has said its piece,
or `null` when it has not started. A `checkpoint` carries the question and the exact coordinates
`situations_checkpoint_answer` requires; a superseded question is not offered. `drafts` are the
prepared emails in full, each waiting as a draft in its sender's own mailbox, capped at ten with the
remainder counted. `account` is what each turn noticed rather than every lookup it made.

### `situations_checkpoint_answer`

Answers an open checkpoint and lets the matter move.

* **Input:** `situation_id`, `message_id`, `tool_call_id`, `branch_index`, `answer`.

Take every coordinate from a `situations_work_read` checkpoint. Any member who can see the matter may
answer, and who answered is recorded. The answer is durable when this returns; the turn it unblocks
runs afterwards, so read the matter back to see what Havilo then prepared.

### `situations_testimony_add`

Records what you know that the record does not.

* **Input:** `situation_ids`, `body`.
* **Limits:** `body` is up to 10,000 characters; `situation_ids` holds 1 to 50 matters.

Work finished outside a mailbox leaves no evidence anywhere, so without this a matter stays open
forever and nothing can honestly close it. This is also the answer when someone wants matters
dropped, cancelled or closed: no such command exists, because a matter is an assessment rather than
a ticket. Your words enter the same ledger as a message or a meeting, and Havilo reasons about them:
they change what is true by being weighed against everything else the matter knows, never by decree.
Repeating the same sentence about the same state of a matter is the same testimony rather than a
second one.

One sentence is often true of several matters at once, so `situation_ids` files it on all of them in
one call. Each matter still gets its own testimony, its own reading and its own outcome — file it
only where it actually holds, because a sentence stretched over a matter it does not fit is a false
claim on that matter. The result carries `recorded`, `refused`, and a `matters` row per id; a matter
that refuses (retracted, no longer active, not visible to you) names its reason there and leaves
every other matter in the call written.

### `situations_draft_revise`

Asks for a change to an email Havilo prepared.

* **Input:** `situation_id`, `artifact_id`, `feedback`.
* **Limits:** `feedback` is up to 2,000 characters.

The draft keeps its artifact id and the draft in your mailbox while Havilo rewrites it in place, on a
side conversation about that draft, so asking for a warmer opening never displaces the account of the
matter. This is for work already prepared; a claim about the world is `situations_testimony_add`. The
draft must be one of yours — seeing a matter is not the same authority as rewriting someone else's
outgoing mail.

## Jobs

Each job tool requires **either** `BULK_OPERATIONS` **or** `AI_FEATURE_ACCESS`.

### `jobs_get`

Returns one job only when it belongs to the connected workspace.

* **Input:** `job_id`.

A run that draws credits also carries `credits_at_most`, what it held when it started, and
`credits_used`, what it was charged: `null` while it runs, `0` if it failed.

The customer catalog does not expose global queues, CloudWatch, message receipt, retries, cancellation, or AWS mutation.
