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.
Shared conventions
- CRM, import, and AI mutation tools default to
apply: falseand return a preview. Situation mutations act immediately after authorization; they do not acceptapply. apply: truecrosses 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_understandandai_planhavereadOnlyHint: 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: truemarks 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, andimports_csv. Preview defaults do not remove this annotation becauseapply: truecan overwrite data. Field changes can also convert stored values, and CSV imports can update matched records.idempotentHint: trueholds 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_suggestreturns an identical open suggestion rather than duplicating it, but declaresdestructiveHint: truebecause a different proposal can supersede the current suggestion. The previous row remains as history.openWorldHint: falseapplies to private imports, table artifacts, drafts, understanding, and planning.ai_runandai_runs_startretaintruebecause 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, optionaldescriptionandcolor, and optionalapply. - Preview permission:
ENTITY_READ. - Apply permissions:
ENTITY_CREATEandENTITY_UPDATE.
crm_fields_ensure
Previews or idempotently creates or updates one field on an existing entity type.
- Input:
entity_type_id,name,type,icon, optionalfield_id,description,required,is_list,options,split_existing_values_on,join_existing_values_with, andapply. - Preview permission:
ENTITY_READ. - Apply permissions:
ENTITY_CREATEandENTITY_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, optionaldescription,forward_label,reverse_label,bidirectional, andapply. - Preview permission:
ENTITY_READ. - Apply permissions:
ENTITY_CREATEandENTITY_UPDATE.
Entities and relationships
crm_records_search
Searches or pages through active Entities.
- Input: optional
entity_type_id,query,external_id,page, andlimit. - Permission:
ENTITY_READ. - Limits:
limitis 1–100 and defaults to 50. Ranked text search is limited to the first 1,000 results; narrowqueryorentity_type_idinstead 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, andtotal_count: nullbecause exact-counting the full match population would repeat the expensive scan. Pagination withoutqueryand exactexternal_idlookup return an exact numerictotal_count. - Output: records carry
entity_type_id, populatedfields,entity_source, andimport_hash. The response does not echo Entity type or field definitions; callcrm_schema_getwhen 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:
recordsand optionalapply. - Existing Entity: provide
record_id,entity_type_id, and afieldsobject keyed by field ID. - New Entity: provide
entity_type_id,fields, and at least one ofexternal_idoridempotency_key. - Preview permission:
ENTITY_READ. - Apply permission when every item has
record_id:ENTITY_UPDATE, orENTITY_UPDATE_LINKEDwhen every record is linked to the caller. - Apply permissions when any item may be created:
ENTITY_CREATEandENTITY_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,fieldskeyed by field name or ID, andreasoning(what was said or read, one sentence, shown to whoever decides). - Permission:
ENTITY_READ. Deciding the suggestion requiresENTITY_UPDATEorENTITY_UPDATE_LINKED. - Result per field:
pending(filed, or an identical open suggestion already stood),unchanged(the record already holds the value), ordeclined(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:
relationshipsand optionalapply. - Relationship input:
relationship_type_id, one reference per end, and optionalqualifier,external_id,title,start_date,end_date, andmetadata. - Preview permission:
ENTITY_READ. - Apply permissions:
ENTITY_CREATEandENTITY_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, optionalsource_type, optionallimit(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, andeditable.
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, optionalrevision,edits, optionalappend, and optionalapply. - 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_stringdoes not appear — the Note has changed, or the text differs in whitespace or punctuation;old_stringis ambiguous — add surrounding context, or setreplace_all;revisionis 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_upsertto 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_idandmarkdown;title,entity_ids, andapplyare optional. - New Note: provide
idempotency_keyandmarkdown;title,entity_ids, andapplyare optional. - Preview permission:
NOTE_READ. - Apply permission with
note_id:NOTE_UPDATE. - Apply permissions without
note_id:NOTE_CREATEandNOTE_UPDATE. - Limits: markdown is at most 1,000,000 characters and
entity_idsaccepts 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, andlimit(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, optionalplan_hash, and optionalapply. - Preview permissions:
ENTITY_READandAI_FEATURE_ACCESS. - Apply permissions:
ENTITY_UPDATE,BULK_OPERATIONS, andAI_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; optionalmatch_filter(all,matched,unmatched),title,rows. - Permissions:
ENTITY_READandAI_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, 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 does; columns picks and orders columns. No AI call.
- Input:
attachment_id,file_name; optionalwhere,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.
- Input:
entity_type_id,file_name. - Permissions:
BULK_OPERATIONS,ENTITY_CREATE, andENTITY_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_idin-app,upload_tokenexternally, orcsv),external_id_column,field_mapping,idempotency_key; optionalwhere,relationship_mapping,note_columns,note_mode,list_mode,list_delimiter, andapply. - Preview permission:
ENTITY_READ. - Apply permissions:
BULK_OPERATIONS,ENTITY_CREATE, andENTITY_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_hashandidempotency_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, optionalchat_modeandapply. - Preview permission:
AI_FEATURE_ACCESS;xrayandglobal_searchalso requireVIEW_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 optionaltitle,rows, and semanticlimit. - Permissions:
ENTITY_READandAI_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_idof the table it wrote. The table is unsaved: it is not in the workspace's Artifacts untilcrm_artifacts_savesaves 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(trueorfalse). - 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_matchorimports_tablefor 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, optionallimit(1–200), optionaloffset, or optional exactrow_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 optionalcc,from,template_note_id,title. - Permission:
ENTITY_READandAI_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
NEWemail saved unbound, in Havilo only; aREPLYis 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, optionalrevision, and one or more ofedits,append,subject,name,to,cc,bcc,request. - Permission:
ENTITY_READandAI_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. 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:
limitis 1–50 and defaults to 25. Defaults to theNEEDS_YOUandWAITINGstandings.
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:
bodyis up to 10,000 characters;situation_idsholds 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:
feedbackis 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.