Back

Clarify

Manage your CRM contacts, deals, and records.

MCP server URL

https://clarify.gumstack.com/mcp

Works with

Tools 47

  • Query Data

    Execute PostgreSQL queries to retrieve data from Clarify. ## When to use this tool - Counting records (e.g., "How many deals are open?") - Aggregations (SUM, AVG, COUNT, MAX, MIN, GROUP BY) - List queries (e.g., "Show me 50 recent meetings") - Filtering by timestamps (e.g., deals created in Q4, meetings last week) - Searching/filtering large datasets (e.g., "Show all deals from Q4" or "Find records updated this week") ## For detailed record context, use get-records After finding record IDs with this tool, use get-records for: - Full record details, relationships, and AI summaries - Meeting recording transcripts with speaker attribution - Email content, subject, body, and participants - Example: query-data to find IDs, then get-records to read details ## When not to use this tool This tool queries existing CRM records. When users want to find new companies or people to add to the CRM (leads/prospects), search for the lead finder tools instead. - Do not use this tool when the user mentions "leads" or "prospects" and wants to source new contacts (e.g., by industry, location, employee count, job title). Search for the lead finder tools that query the lead database instead. - Still use this tool when the user mentions "leads" or "prospects" but the context clearly refers to existing CRM records (e.g., "leads in my pipeline", "prospects with last product activity in last 30 days"). Activity history, emails, meetings, and pipeline stages only exist for CRM records, not in the lead database. But the user is still asking about leads/prospects, so we still use this tool. - Ask the user to clarify when they search for companies or people without using the word "lead" or "prospect", and the conversation does not already establish a CRM context. Ask whether they want to find new leads/prospects to add to the CRM, or look up existing companies/people already in the CRM. If the conversation already establishes CRM context, use this tool without asking. ## Before writing SQL - This tool does not include any schema. Workspaces have custom fields and JSONB columns that vary per workspace, so you cannot know the column names without loading the schema first. Always call get-schema with format "read" and the entities you need before writing SQL, unless the schema for those entities was already returned by get-schema earlier in this conversation. - Call get-current-user if the query involves "me", "my", or "I", if the tool is available - Always prefix columns with table names (e.g., `person.name`, not `name`) - Use ILIKE for case-insensitive matching (not LIKE) - Query timestamps using UTC/ISO 8601 format (e.g., '2025-12-22T00:00:00Z') - Results will have timestamps formatted in the user's timezone for readability - For many-to-many relationships, use INNER JOIN with join tables ## Working with JSONB Columns Many columns store data as JSONB objects. Check the schema returned by get-schema to identify JSONB columns. Only use JSONB operators (->, ->>) on columns explicitly marked as "JSONB with format" in the schema. ### JSONB Access Operators - `->` - Extract JSON object (returns JSONB): `column->'key'` - `->>` - Extract text value (returns TEXT): `column->>'key'` - `jsonb_array_elements(column->'items')` - Iterate over JSONB arrays ### JSONB Column Patterns Object with nested fields (e.g., `JSONB with format {first_name: string, last_name: string}`): - Access nested field: `column->>'first_name'` - Filter: `column->>'first_name' ILIKE '%john%'` Array of items (e.g., `JSONB with format {items: string[]}`): - Get first item: `column->'items'->>0` - Iterate items: `jsonb_array_elements(column->'items') as item` - Access nested in item: `(item->>'email')` ### Filtering multi-select and label arrays For columns with format `{items: string[]}` (multi-select enums, the built-in `labels` field, etc.), filter membership with the array-overlap operator `?|`: - Has any of: `(person.labels -> 'items') ?| ARRAY['BDR Prospect', 'ICP']` Do not use the `@>` containment operator or `CONTAINS`. `@>` works in the database, but a list built with it cannot be rendered or edited in the list UI; always use the `?|` pattern above. ### Checking if JSONB collection fields are empty For columns with format `{items: string[]}` (multi-select enums, email addresses, etc.), an empty field can be either NULL or `{"items": []}`. Always check for both: - Is empty: `(column IS NULL OR (column->'items') = '[]'::JSONB)` - Is not empty: `(column IS NOT NULL AND (column->'items') <> '[]'::JSONB)` Do not use only `IS NULL` or `IS NOT NULL` for these fields; that misses records with empty arrays. ## Time-Based Queries Use the current time from your context as the reference. ### "Last" vs "Next" Terminology - "last meeting" = Most recent past meeting: `WHERE meeting.start < NOW() ORDER BY meeting.start DESC LIMIT 1` - "next meeting" = Soonest future meeting: `WHERE meeting.start > NOW() ORDER BY meeting.start ASC LIMIT 1` - "recent meetings" = Meetings in the past, not upcoming ones - "upcoming meetings" = Meetings in the future ### Time Period Interpretation When user mentions "Q4 2025", "last quarter", or similar: - Activity in that period: Filter by date: `WHERE meeting.start >= '2025-10-01' AND meeting.start < '2026-01-01'` - Content mentioning that period (e.g., "meetings where Q4 was discussed"): Search transcripts/summaries, not date filter ## User-Scoped Queries When querying data related to "me", "my", or "I": - Use the current user's ID to filter records - Remember: `user` and `person` are different tables - `user` = internal workspace members, `person` = external contacts - For meetings: `INNER JOIN user_meeting ON meeting._id = user_meeting.meeting_id WHERE user_meeting.user_id = '<user-id>'` - External meetings have attendees who are not workspace users. Check `person_meeting` but exclude persons whose email matches a user: `EXISTS (SELECT 1 FROM person_meeting pm JOIN person p ON p._id = pm.person_id WHERE pm.meeting_id = meeting._id AND NOT EXISTS (SELECT 1 FROM "user" u WHERE u.email IN (SELECT jsonb_array_elements_text(p.email_addresses->'items'))))` - Internal meetings have no truly external attendees (inverse of above) - For tasks: Filter by `assignee_id`: `WHERE task.assignee_id = '<user-id>'` - For deals: Filter by `owner_id`: `WHERE deal.owner_id = '<user-id>'` ## Deal Analytics ### Comparison operators for thresholds When users reference a threshold amount: - "above $300", "over $300", "at least $300": `deal.amount >= 300` - "below $500", "under $500": `deal.amount < 500` - "more than $300", "fewer than 5": use strict comparison (`>` / `<`) ### Time-bounded deal metrics When users ask about deals in a specific time period (e.g., "January deals", "Q4 closed deals", "deals closed this month"), always filter by `close_date` within that period in addition to any stage filter. Do not filter only by stage -- that returns all historically closed deals, not deals closed in the requested period. Only add a closed stage filter when the user is asking about closed or won deals specifically. Do not add a closed stage filter for queries about open or in-progress deals. - Example: "deals closed in January 2026" requires `deal.stage IN (<closed stages from schema>) AND deal.close_date >= '2026-01-01' AND deal.close_date < '2026-02-01'` - Example: "deals expected to close in Q1" should only filter by `close_date` without a closed stage constraint ## Association Queries When finding emails or meetings "related to" a company or entity, join through the relationship graph instead of searching message body text. - Emails related to a company: JOIN person_message with person WHERE person.company_id = <company_id>. Do not use raw_body ILIKE '%company name%' -- body text matching picks up incidental mentions and links unrelated contacts. - Meetings related to a company: JOIN person_meeting with person WHERE person.company_id = <company_id>. - Emails for a specific person: JOIN person_message WHERE person_message.person_id = <person_id>. - Emails related to a deal: JOIN person_deal (deal -> person) then person_message: deal -> person_deal -> person -> person_message -> message. - Meetings related to a deal: JOIN person_deal (deal -> person) then person_meeting: deal -> person_deal -> person -> person_meeting -> meeting. - Only use body/subject text search when the user explicitly asks to search email content (e.g., "find emails mentioning Project X"). ## Activity Counting Activity includes: meetings, messages, tasks, and comments. To count activity: - Comments: Query `comment` table with `entity = '<entity-type>'` and `owner_id = <entity>._id`. Use `_created_at` as timestamp. - Meetings: Join through `person_meeting` join table. If entity relates to `person` via join table (e.g., `person_deal`) or foreign key (e.g., `person.company_id`), join: entity → person → `person_meeting` → `meeting`. Use `meeting.start` as timestamp. - Messages: Join through `person_message` join table. Same pattern as meetings: entity → person → `person_message` → `message`. Use `message.received_at` as timestamp. - Tasks: Join via foreign key `task.deal_id`. For deals: direct join. For companies: join `deal` first (via `deal.company_id`), then `task`. Use `task._created_at` as timestamp. Pattern: Create CTEs for each activity type, UNION ALL them, then LEFT JOIN to your entity table. Use COUNT() and MAX() for totals and last activity date. ## Comments Comments are notes left on records. The comment table has no schema in get-schema: use these columns directly: - `_id`: UUID primary key - `entity`: parent entity type (e.g., 'company', 'deal', 'person', 'meeting', 'task') - `owner_id`: parent record ID - `message`: JSONB rich text; use `message::text` for raw content - `_created_by`: user ID of the comment author. JOIN with `"user"` on `_created_by = "user"._id` for author name - `_created_at`: timestamp ## Existence Checking Before creating a record, search broadly by name to check if it already exists. Duplicate records with the same name but different IDs are common. - Use ILIKE name matching: WHERE deal.name ILIKE '%Snowplow%' or WHERE company.name ILIKE '%Snowplow%'. Do not rely solely on a single foreign key (e.g., WHERE deal.company_id = '<id>') -- this misses records linked to duplicate/variant company records. - When checking for existing deals, search by both the deal name and the related company name. ## Name search strategies When a name search returns no results, try variations before asking the user to clarify. Records are often stored differently than expected (e.g., "BrightLoop" vs "Bright Loop"): - Remove spaces: `ILIKE '%brightloop%'` - Search individual keywords: `name ILIKE '%bright%' OR name ILIKE '%loop%'` - For a person's first name, also try common nickname/diminutive variants (e.g. Tim/Timothy, Bob/Robert, Liz/Elizabeth, Bill/William, Mike/Michael) and a prefix match (`name->>'first_name' ILIKE 'Tim%'`) before concluding no match exists - If a broad search returns many results, pick the closest match rather than asking the user to choose Before matching a name column with ILIKE, check its type in the get-schema output. A name stored as a JSONB object (shown as `JSONB with format {...}`) is not text: a bare `name ILIKE` on it fails with `operator does not exist: jsonb ~~*`. This is common for the display name on people-type entities (for example a person or workspace user whose name is `{first_name, last_name}`). Extract the text first, e.g. `name->>'first_name' ILIKE '%...%'` or `CONCAT(name->>'first_name', ' ', name->>'last_name') ILIKE '%...%'` (see "Working with JSONB Columns" above). Only columns the schema marks scalar (e.g. a plain `string` email) work with a direct ILIKE. ## Meetings Meetings are personal data, like emails: a meeting belongs to its participants (the users with a `user_meeting` row), not to the whole workspace. Meeting content can be sensitive, so scope meeting queries to the current user by default. - Default: return only the current user's own meetings by INNER JOINing `user_meeting` (see User-Scoped Queries for the exact join). This applies to bare requests with no other anchor -- "today's meetings", "recent meetings", a daily/EOD digest -- exactly as you filter emails to the current user. - Only query across all users' meetings when the user explicitly asks for the team's or workspace's meetings. - Meetings anchored to a specific company, person, or deal are the exception: join through `person_meeting` (see Association Queries) instead of `user_meeting`. ## Meeting Recordings & Transcripts Meeting recordings are linked to meetings via meeting_recording.meeting_id. The meeting_recording table is queryable via SQL (use entity=meeting for the query): - Find recordings: SELECT meeting_recording._id FROM meeting_recording WHERE meeting_recording.meeting_id = '<meeting-id>' - Read transcript: use get-records with entity=meeting_recording and the recording IDs A meeting can have multiple recordings. To avoid duplicate rows, filter with an EXISTS subquery instead of JOINing meeting_recording directly: SELECT meeting._id, meeting.title FROM meeting WHERE EXISTS (SELECT 1 FROM meeting_recording WHERE meeting_recording.meeting_id = meeting._id) When listing or showing meetings (e.g., "show me my last 5 recorded meetings"): - Always include summary and/or notes in the SELECT columns, not just metadata (title, date, status) - Present the summary content inline so the user sees what each meeting was about - If summaries are available, offer to show full transcripts for any specific meeting When answering questions about a single meeting (summary, follow-up email, action items): - Check for recordings and read the transcript via get-records before responding When answering questions across multiple meetings: - Prefer using the summary and notes fields first (available on the meeting record) - Only read individual transcripts if the user explicitly asks or summaries are insufficient Meeting content fields: - "summary": AI-generated meeting summary (on meeting record) - "notes": User-written meeting notes (on meeting record, separate from summary) - If summary is null, check notes before concluding the meeting has no content A substring match inside a summary is not evidence that a topic was requested or discussed. Summaries often enumerate ABSENT topics ("No mention of dialers, enrichment, or reporting tools"), so an ILIKE '%dialer%' hit can be a negation. Before attributing a request or interest to a person or meeting from a summary match, read the matching summary text and confirm the term appears in an affirmative context. Do not count a negated mention as a positive signal, and do not report a corrected count as fact until you have re-read the source. ## Emails & Messages The message table contains sent and received emails. Use entity=message to query directly. Sensitive columns (raw_body, subject) are protected by database-level RLS; only messages the current user has access to will return content. Use get-records with entity=message for full formatted email content with participant details. Filter by user_id for the current user's emails, received_at for time ranges, and person_message join for specific contacts. Never claim you cannot access emails -- sent and received emails are queryable. Unsent drafts are not. ## Pagination - When user asks for "more" results, use the EXACT same SQL query and only change the offset parameter - The tool will tell you the next offset value to use (e.g., "Call this tool again with offset=25 to continue") ## Data Limitations Tables contain current field values only. You cannot determine what a field's value was at a past date with this tool. What you can answer: - Deals currently in stage X: `WHERE deal.stage = 'In progress'` - Deals created/updated in a period: `WHERE deal._created_at >= '2025-10-01'` - Deals closed in a period: `WHERE deal.stage IN (<closed stages from schema>) AND deal.close_date >= '2025-10-01' AND deal.close_date < '2025-11-01'` (see "Time-bounded deal metrics" above) What requires the query-analytics tool instead: - "How many deals were in stage X on December 1st?" (requires historical snapshots) - "What was the pipeline value entering Q4?" (requires reconstructing past state) - "Which deals changed stage last month?" (requires stage transition history) For historical or point-in-time questions like these (past field values, stage transitions, change history, trends over time), use the query-analytics tool. It queries the analytics event log of every CRM change and can reconstruct past state.

  • Query Analytics

    Execute read-only ClickHouse SQL against the analytics event log. ## When to use this tool Use `query-analytics` for **historical / point-in-time** questions about CRM records: anything that requires looking at the sequence of changes over time, not just the current state. Examples: - "What was the sequence of stage changes for this deal?" - "How long did this deal sit in 'In progress'?" - "What was the total amount of all deals in each stage on a specific date?" - "How many deals were created per day last week?" - "Who last edited this record, and when?" ## When NOT to use this tool For **current state** questions ("which deals are open?", "what's this company's domain?"), use the `query-data` tool: it queries PostgreSQL directly and is faster for live values. Only fall back to `query-analytics` when you need history. ## How it works - The query runs against ClickHouse, not PostgreSQL. Use ClickHouse SQL syntax (`JSONExtractString`, `toDate`, `argMax`, etc.). - When the user hasn't asked for a specific time frame, scope the query to the last 30 days with `AND timestamp >= now() - INTERVAL 30 DAY` to keep it fast and the results relevant. - Statements are read-only: INSERT / UPDATE / DELETE / DDL are rejected by ClickHouse. - Results are capped to the `maxResultRows` parameter (default 1024). Rows beyond the cap are dropped server-side, even if your SQL omits a `LIMIT`. # Analytics database (ClickHouse) Historical event log of every change to the CRM. **Read-only.** Query it to answer point-in-time and change-history questions like: - "What was the sequence of events that led to the current stage of this deal?" - "How long did this deal sit in the 'In progress' stage?" - "How many deals were created per day last week?" ## The `analytics.event` table (ClickHouse) Read-only SQL runs against ClickHouse, not PostgreSQL — use ClickHouse syntax (`JSONExtractString`, `toDate`, `argMax`, etc.). Every CRM record lives in ONE append-only CDC event log, `analytics.event` — NOT a table of current records; every create/update/delete is its own row. There is no `deal` / `company` / `person` table (`FROM deal` fails with "Unknown table expression identifier"). Always query `FROM analytics.event` and pick the entity with `WHERE entity_type = '<type>'`. Hot keys are promoted to materialized columns (below); every other field is read from `properties` with `JSONExtract*`. Every event carries a FULL snapshot of the record's fields in `properties` — create, update, and delete alike. An update is not a delta: it repeats every field the record has, not just the ones that changed. So the single latest event per entity already holds every current field, and you never need to stitch fields together across events. (The one exception: a field you can't access is absent from `properties`.) - `entity_type` (String) — Subject type: `deal`, `company`, `person`, `meeting`, `message`, or `c_<slug>` for a custom object - `entity_id` (String) — Subject record id - `type` (String): `clarify:create`, `clarify:update`, `clarify:delete`, or `clarify:merge` (an update to the record that survived a merge; the merged-away records get a `clarify:delete`) - `timestamp` (DateTime64(6,'UTC')) — Event time; use `toDate(timestamp)` for day grouping - `properties` (String/JSON) — A full snapshot of the record's fields at event time (every field, not just the changed ones); read with `JSONExtractString/Float/Bool(properties, '<field>')` - `diff` (String/JSON) — On update events, the field-level changes this event applied: a JSON array of `{op, path, val, oldVal}` (`path` locates the field, `oldVal` → `val` is the transition). Empty (`''`) on create and no-op events. Use it to detect WHEN a field changed — see "Detecting when a field changed" - `actor` (Tuple): `actor._id`, `actor.entity`, `actor.source_id` (all String, native paths, no `JSONExtract` needed); usable in `GROUP BY` / `ORDER BY` - Use `actor.entity` and `actor.source_id` to tell an automated actor (an agent or an API key) from a human user, and group or exclude by them when an automation may skew a metric - `m_stage` (String) — Materialized `JSONExtractString(properties,'stage')`; empty if absent - `m_amount` (Float64) — Materialized `JSONExtractFloat(properties,'amount')`; `0` if absent - `m_close_date` (Nullable(Date)) — Materialized `toDateOrNull(properties.close_date)`; `NULL` if absent/unset. A native date — range-filter and group it directly (`m_close_date >= '2026-01-01'`, `toStartOfMonth(m_close_date)`) with no `toDate` wrapper ### Reading fields is ClickHouse, not PostgreSQL PostgreSQL JSONB operators `->` and `->>` do not exist in ClickHouse — `properties->>'description'` fails. Read fields with `JSONExtractString(properties, 'description')` (or `JSONExtractFloat` / `JSONExtractBool`). There is no `JSONExtractFloat64` — use `JSONExtractFloat`. If a query fails because a function does not exist, use the alternative ClickHouse suggests (e.g. `JSONExtractRaw`). ### Materialized columns Prefer the materialized columns `m_stage` / `m_amount` / `m_close_date` over `JSONExtract*(properties, 'stage'|'amount'|'close_date')` when the key matches — they are typed, indexed, and skip a JSON parse per row. They are populated from whatever `properties.stage` / `properties.amount` / `properties.close_date` carries on each row, regardless of `entity_type`, so they work for a custom object with those fields too. For `m_stage` / `m_amount`, empty / `0` means the event didn't carry the key. `m_close_date` instead uses `NULL` for an absent or unset close date (a date has no neutral sentinel), so test presence with `m_close_date IS NOT NULL`, not `!= ''`. `m_stage` (like every snapshot field) is the value the record held *when the event fired*, not a transition. Counting `m_stage = 'Won'` answers "records that were in Won while something happened to them" — an enrichment sweep, a note, or an owner change re-emits the unchanged stage — so it tracks activity, not outcomes. For "records that *entered* a stage", read `diff` instead (see "Detecting when a field changed"). ### Write fast queries - Always filter by `entity_type`; add `entity_id` for single-record questions. Events are stored in that order, so these filters read a small slice. Filtering only on other columns (`type`, `m_stage`, `m_amount`, `m_close_date`, any `JSONExtract*` value) scans every entity. - Bad: `SELECT count() FROM analytics.event WHERE m_stage = 'Won'`. - Good: `... WHERE entity_type = 'deal' AND m_stage = 'Won'`. - Give time-series, activity, and trend queries a `timestamp` range (`timestamp >= now() - INTERVAL 90 DAY`) to scope the report and bound the scan. Current-state reconstruction is the exception (below): it needs each entity's full history, so do NOT put a `timestamp` floor on it. - Name the columns you need, and avoid `SELECT *`: it computes the `properties` JSON for every row. - Bad: `SELECT * FROM analytics.event WHERE ...`. - Good: `SELECT entity_id, timestamp, m_stage FROM analytics.event WHERE ...`. - Group by low-cardinality columns (`entity_type`, `m_stage`, `toDate(timestamp)`, `entity_id`). Grouping by a freeform `JSONExtractString(properties, '<field>')` of a note or free text parses JSON per row and explodes the group count. - To eyeball a few recent rows, use `ORDER BY timestamp DESC LIMIT <n>`. - Date literals: prefer relative helpers (`now() - INTERVAL 1 MONTH`, `toStartOfMonth(now())`, `today() - 7`). You have no reliable wall clock, and a hardcoded boundary silently drifts. A bare `timestamp >= '2026-04-01T00:00:00Z'` rejects the ISO `T`/`Z`; use space-separated `'2026-04-01 00:00:00'`, `parseDateTime64BestEffort('2026-04-01T00:00:00Z')`, or `CAST('2026-04-01T00:00:00Z' AS DateTime64(6, 'UTC'))`. ### Reconstruct current state from the log To report on CURRENT state, rebuild it from the event history: - Every event stores a full snapshot of the record, so reconstruct current field values with plain `argMax(<field>, timestamp)` grouped by `entity_id` — never a `JSONHas` / `<field> != ''` / `!= 0` presence guard, which resurrects a stale earlier value for a field that was later cleared or set back to 0. Read a JSON field with `argMax(JSONExtractString(properties,'<field>'), timestamp)`, or a materialized column directly with `argMax(m_stage, timestamp)`. - Drop deleted records: `HAVING argMax(type, timestamp) != 'clarify:delete'`. - Reconstruct in a CTE, then filter/aggregate over the CTE — filtering a mutable field inside the per-entity scan changes which event counts as "latest" and produces wrong totals. ```sql WITH deal_current AS ( SELECT entity_id, argMax(m_stage, timestamp) AS stage, argMax(m_amount, timestamp) AS amount FROM analytics.event WHERE entity_type = 'deal' GROUP BY entity_id HAVING argMax(type, timestamp) != 'clarify:delete' ) SELECT stage, count() AS deals, sum(amount) AS pipeline FROM deal_current WHERE stage NOT IN ('Closed Won', 'Closed Lost') GROUP BY stage ``` ### Count stage transitions in a period A snapshot count (`m_stage = 'Won'`) can't answer "how many deals were won this quarter" — it counts deals *touched* while already won. Count the transition instead: the events where `stage` moved into a closed value, plus deals created directly in one. The `UNION` branch is required because `diff` is empty on create — it catches deals imported straight into a closed stage. ```sql -- Deals that ENTERED a closed stage inside the window. WITH closed AS ( SELECT entity_id, timestamp, m_stage AS stage_at_event FROM analytics.event WHERE entity_type = 'deal' AND type = 'clarify:update' AND arrayExists(d -> JSONExtractString(d, 'path', 1) = 'stage' AND JSONExtractString(d, 'val') IN ('Won', 'Lost'), JSONExtractArrayRaw(diff)) UNION ALL -- diff is empty on create: catches deals imported directly into a closed stage SELECT entity_id, timestamp, m_stage FROM analytics.event WHERE entity_type = 'deal' AND type = 'clarify:create' AND m_stage IN ('Won', 'Lost') ) SELECT countIf(final_stage = 'Won') AS deals_won, count() AS deals_closed, countIf(final_stage = 'Won') / nullIf(count(), 0) AS win_rate FROM (SELECT entity_id, argMax(stage_at_event, timestamp) AS final_stage FROM closed GROUP BY entity_id) ``` The outer `argMax` collapses each deal to its last transition in the window, so a deal that goes Won → Lost inside it counts once, as Lost. There is no delete guard here on purpose — a deal won in the window was won even if the record was later deleted; to drop since-deleted deals, reconstruct current state (above) and keep only the `entity_id`s that are still live. Matching on `val` works when you can access the field; for one you can't, `val` reads `'REDACTED'` (its `path` is kept), so match on `path` presence alone. For a "closed per month" trend, bucket by the event `timestamp` — that is when the transition actually happened. One data limitation to surface, not hide: on a batch-imported workspace every `clarify:create` lands on the import date, so deals that closed before they were imported collapse onto that date — the log has no real historical close date for them. `m_close_date` does not fill that gap: it is the *expected* close date (often in the future), right for a pipeline-by-expected-close forecast but wrong as a "when did we win" bucket. When you do bucket a retrospective metric by a date field that can run into the future (`m_close_date`, any custom date field), cap the buckets to the current period — `AND m_close_date <= today()`, or `HAVING bucket <= toStartOfMonth(today())`. A future bucket holds only open pipeline, so a "what happened" metric (win rate, deals won) reads there as losses or zeros. Drop the cap only for an intentional forward-looking pipeline/forecast. ### Detecting when a field changed A field being non-empty on an event does NOT mean it changed on that event — every event repeats all fields, so an edit to one field re-emits the rest unchanged. Use the `diff` column: on an update it lists exactly the fields that changed, so "did this field change on this event" is a plain row filter. Unlike comparing snapshots across events, this composes with a `timestamp` range and stays fast. A field changed on an event when `diff` holds an entry whose first `path` element is that field: `arrayExists(d -> JSONExtractString(d, 'path', 1) = 'stage', JSONExtractArrayRaw(diff))`. The matching entry also carries the transition — `oldVal` → `val` — so you can report "moved from X to Y" without reading other events. Caveats: `diff` is populated on `clarify:update` events only — it is empty on `clarify:create`, so if you also need the value set at creation (e.g. a deal's first stage), `UNION` in the create event. A change to a field you can't access shows `val`/`oldVal` as `'REDACTED'` but keeps its `path`, so the change is still detectable. Do NOT count a stage question off the snapshot: `m_stage != ''` counts every edit to a record that has a stage, and `m_stage = 'Won'` counts every edit to an already-won deal — neither is a stage change. Match on `diff` instead (see "Count stage transitions in a period"). ### Join across entities Every entity lives in the same `analytics.event` table, so a cross-entity report is CTEs joined on a foreign key stored in `properties`. Reconstruct each entity's current state in its own CTE (the pattern above), then join on the id — a deal's `company_id` links to the company's `entity_id`. Scope the looked-up CTE to only the ids the join needs: add `AND entity_id IN (SELECT <fk> FROM <driving_cte>)` to its `WHERE`. `entity_id` is a sort-key column, so this prunes reconstruction to a tiny slice instead of rebuilding every record of that entity type — the difference between a many-second and a sub-second query. ```sql WITH deal_current AS ( SELECT entity_id, argMax(JSONExtractString(properties,'company_id'), timestamp) AS company_id, argMax(m_amount, timestamp) AS amount FROM analytics.event WHERE entity_type = 'deal' GROUP BY entity_id HAVING argMax(type, timestamp) != 'clarify:delete' ), company_current AS ( SELECT entity_id, argMax(JSONExtractString(properties,'name'), timestamp) AS name FROM analytics.event WHERE entity_type = 'company' AND entity_id IN (SELECT company_id FROM deal_current) GROUP BY entity_id HAVING argMax(type, timestamp) != 'clarify:delete' ) SELECT c.name AS company, sum(d.amount) AS pipeline FROM deal_current d JOIN company_current c ON c.entity_id = d.company_id GROUP BY c.name ORDER BY pipeline DESC ``` ### Common pitfalls - Summed or counted returned rows by hand: you totaled values in your reasoning instead of in the query. Push it into the query (`sum(...)`, `count()`, `avg(...)`, `countIf(...)`) over the reconstructed CTE, and read the total from the result. - Pasted a literal id list into `IN ('…','…')`: you transcribed ids from a prior result. Filter with a subquery instead: `entity_id IN (SELECT <fk> FROM <cte>)` (the join pattern above). ### Time-in-stage Select the stage-transition events with the `diff` filter (see "Detecting when a field changed"), and `UNION` in each deal's `clarify:create` event so the first stage's clock starts at creation. Then, over that filtered set, take each transition's gap to the next with `leadInFrame(timestamp) OVER (PARTITION BY entity_id ORDER BY timestamp ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)` — that gap is how long the deal held the stage. The frame is required: `leadInFrame` defaults to a frame ending at the current row and would never see the next event. The latest transition has no next event, so its `leadInFrame` is the zero date — treat that as "still in this stage" (`now()`). Don't treat every `clarify:update` as a transition — every event repeats the current stage whether or not it changed. ## Examples <example> -- Deals created per day in the last 7 days SELECT toDate(timestamp) AS day, count() AS created FROM analytics.event WHERE entity_type = 'deal' AND type = 'clarify:create' AND timestamp >= now() - INTERVAL 7 DAY GROUP BY day ORDER BY day </example> <example> -- Stage changes for a single deal (only the events where the stage actually -- changed — a non-empty stage alone is not a change, since every event repeats -- all fields). `diff` lists the fields an update changed, so keep the events -- whose diff touched `stage`; the matching entry carries the old→new value. SELECT timestamp, m_stage AS stage, arrayFirst(d -> JSONExtractString(d, 'path', 1) = 'stage', JSONExtractArrayRaw(diff)) AS stage_change, JSONExtractString(stage_change, 'oldVal') AS from_stage, JSONExtractString(stage_change, 'val') AS to_stage, actor._id AS actor_id FROM analytics.event WHERE entity_type = 'deal' AND entity_id = '<deal-id>' AND type = 'clarify:update' AND arrayExists(d -> JSONExtractString(d, 'path', 1) = 'stage', JSONExtractArrayRaw(diff)) ORDER BY timestamp </example> <example> -- Stage-change events per day across all deals. `diff` records the fields each -- update changed, so a stage change = an update whose diff touched `stage`. No -- window function and no boundary caveat: each event is self-contained, so the -- timestamp range is exact. SELECT toDate(timestamp) AS day, count() AS stage_changes FROM analytics.event WHERE entity_type = 'deal' AND type = 'clarify:update' AND arrayExists(d -> JSONExtractString(d, 'path', 1) = 'stage', JSONExtractArrayRaw(diff)) AND timestamp >= now() - INTERVAL 90 DAY GROUP BY day ORDER BY day </example>

  • Get Current User

    Get information about the current authenticated user, including their timezone. ## When to use this tool Use this to understand who "me", "my", and "I" refer to in user queries. ## Important notes When interpreting relative dates (e.g., "last 2 weeks", "yesterday"): - Calculate dates in the user's timezone returned by this tool - Convert to UTC and ISO format for database queries

  • Get Schema

    Get the schema for Clarify entities. ## When to use this tool Use the read format when working with SQL queries and the query-data tool. Use the write format when creating or updating records with the create-or-update-records tool. Pass the specific entities you need. Read defaults to the CRM database, and omitting entities returns every CRM entity, a large result that is re-sent on every later turn, so only omit it when you genuinely need the whole model. A read renders each requested entity's columns plus join-table stubs for its relationships, enough to write JOINs. To read a related entity's own columns, request that entity too. For lead columns (funding stage, employee range, industries, location), request the tam_company or tam_person entities to get the schema for the find-leads tool. The analytics event log schema comes with the query-analytics tool directly, so you do not need this tool for it. ## Databases Clarify has three databases: - CRM database (company, person, deal, meeting, user, task): your workspace data. Use with the query-data tool. Read returns these by default. - Leads database (tam_company, tam_person): prospecting data for finding new leads. Request these entities explicitly. Use with the find-leads tool. - Analytics database (ClickHouse, single `analytics.event` table): historical event log of every CRM change: point-in-time and change-history questions. Its schema comes with the query-analytics tool. ## Formats - read: schema for reading data via SQL queries with the query-data tool, or lead columns for the find-leads tool. Returns all columns, types (including JSONB structure), relationships, join tables, and custom fields. Defaults to CRM entities. - write: writable fields for the create-or-update-records tool. Returns fields that can be set when creating or updating records.

  • Read Context

    Read Clarify product knowledge and documentation. Use this whenever you need to understand a feature, apply best practices, or reference detailed technical documentation. Available contexts: - field-docs: Field type guidelines, AI prompt writing instructions, available relationship data, and examples for creating or updating custom fields - workflow-docs: Documentation about how the Workflows feature works. Workflows are event-based programmatic automation of the CRM. - calendar-docs: Guide for the calendar tools: timezone handling, timed vs all-day events, resolving participants by name via CRM lookup, participant rules (always include the user), choosing which connected account a create lands on (other writes follow the event owner), update-replaces-not-merges semantics, recurrence (RRULE), notify_participants defaults, and event-ID conventions. - campaign-docs: Complete guide to campaigns including steps and waits, variables, enrollment, exit conditions, and best practices - campaign-linkedin-steps: How to build LinkedIn steps (visits, connection requests, messages, InMails) in campaigns - artifact-docs: The required data rules for authoring an artifact: which store its SQL runs on, how to prove a query, and how to size a result for the frame, plus what makes a dashboard, QBR, or sales document effective. Read before you write SQL for a dashboard, report, or artifact. - list-docs: How to write a list's defining SQL: title alias, showing to-one relationships, and column restrictions

  • Get Records

    Retrieve detailed information about specific records by their IDs. This tool provides comprehensive context about records, including relationships, AI summaries, and other details. Use this tool when you need to: - Understand the current state of specific records before answering questions or taking actions - Read meeting recording transcripts with full speaker attribution - Read email content, subject, body, and participants - Get rich context about records found via query-data Typical workflow: Use query-data to find record IDs, then pass them here for full details. Meeting-specific guidance: - For meeting: returns metadata, summary (AI-generated), notes (user-written), and associated recordings. If summary is null, check notes before concluding no content exists - For meeting_recording: returns the full transcript with speaker names and timestamps - When the user asks for exact quotes, verbatim wording, or "what exactly was said", fetch the meeting_recording transcript via get-records. The meeting summary is paraphrased and does not preserve the original words.

  • Create Or Update Records

    Create or update records in Clarify. Supports bulk operations of up to 25 records per call. - **Without id**: Create a new record - **With id**: Update an existing record by ID This tool also links and unlinks relationships between records, not just attributes -- see the Relationships section below for trigger words and examples. All records in a single call must be the same operation: either all creates (no `id`) or all updates (with `id`). Split mixed batches into two separate calls. Each call is atomic: if any record in the batch fails, the entire batch is rolled back and no records are written. When working with user-supplied IDs (CSVs, pasted lists) verify the IDs exist with query-data first, or use smaller batches to limit the blast radius of a single bad ID. ## Before creating or updating records - Writable fields are not included in this tool's description. You must call get-schema with format "write" and only the entities you need before creating or updating records, unless the write schema for those entities was already loaded earlier in this conversation. Do not guess field names. - Never tell a user a field can be set before confirming it appears in the write schema. Computed / read-only fields (system-managed, e.g. interaction dates) are deliberately omitted from the write schema: they cannot be set here, and a field missing from the write schema is read-only or nonexistent, not something to recreate as a custom field. Do not offer to set such a field or to create a custom field of the same name. Explain that it updates automatically (from meetings and emails) and offer the real workaround, such as logging a meeting. - Before creating a company, deal, or person, use the query-data tool to search for existing records by name (ILIKE). Use a broad search, not a narrow ID filter. - If the search returns potential matches, present them to the user and ask whether to use an existing record or create a new one. - If no matches are found, proceed to create without asking for confirmation. - When creating a meeting, provide `title` (string), `start` (ISO 8601 datetime), and `end` (ISO 8601 datetime) in attributes. Set attendees via the `participants` attribute using the format `{ set: [{ email: "alice@example.com", name: "Alice" }, ...] }`. People, companies, and associations are created automatically from participants. - Meetings also support `summary` (markdown string for meeting summary/recap) and `notes` (markdown string for meeting notes or transcript). Pass these as plain markdown strings in attributes. - If the meeting data includes a timezone, pass it as the `timezone` attribute (IANA timezone string like "America/Chicago"). Naive datetimes in `start`/`end` will be interpreted in that timezone. If no timezone is provided, the user's preferred timezone is used. UTC datetimes (with Z or offset) are stored as-is. ## Relationships Pass all relationships (to-one and to-many) via the `relationships` parameter using this format: `[{ "relationshipFieldName": "<field>", "operation": "link" | "unlink", "targets": [{ "entity": "<entity>", "_id": "<id>" }] }]` Use `operation: "link"` or `operation: "unlink"` when the user says link, unlink, connect, disconnect, associate, dissociate, or relate -- any create/remove of a relationship between two records, for example: - "Link this deal to Acme Corp" -> link - "Unlink this contact from the deal" -> unlink - "Connect this deal to Acme Corp" -> link - "Dissociate this contact from the deal" -> unlink A relationship-only call is valid: `attributes` can be omitted (it defaults to `{}`) when only `relationships` is populated, e.g. to link or unlink a record without changing any other field. The specific relationship fields available per entity are returned by get-schema with format "write". ## Messages are link-only Messages (email and LinkedIn) are synced from the provider — you cannot create a message or edit its fields here. You can only correct who a message is linked to. Pass the message `id` with a `people` relationship (link or unlink) and no `attributes`, for example to attach a person to a message that was ingested without the right contact. The relationship field is `people` and its targets are `person` records. ## Creating deals and tasks When creating a deal or a task, do not create a bare record with only a name. Fill in the fields that make it useful: inferring what you can and asking the user only for what you genuinely cannot. - **Owner / assignee defaults to the current user.** Unless the user names someone else, set a new deal's `owner_id` (and a new task's `assignee_id`) to the current user. Pass it via `relationships`, e.g. `{ "relationshipFieldName": "owner_id", "operation": "link", "targets": [{ "entity": "user", "_id": "<current user id>" }] }` (use `assignee_id` for tasks). The current user's record ID is in the "Information about the user" section of your context; MCP clients can get it from get-current-user. - **Infer before asking.** Before creating a deal, try to fill `amount`, `close_date`, and `stage`, and to link the relevant company and people, from context already available to you: recent meetings and emails with that company, and existing related records (use query-data and get-records). Link the named company (creating it first if it does not exist, per the notes below) and any clearly-relevant people. - **Ask only for the gaps.** If a field that matters can't be confidently inferred, ask the user for it in one short round rather than silently creating an empty record, and let them skip. Do not block creation if the user clearly just wants a quick record. - **Amount is a committed actual.** Never invent or estimate a deal's `amount`. If you don't have a committed, agreed value, ask the user or leave it unset; do not guess from projections. ## Collection fields (labels, multi-select, emails, etc.) Collection fields require an explicit operation. Do not pass raw arrays or { "items": [...] }. - **Append** (add values): `{ "append": ["value1", "value2"] }` - **Remove** (remove values): `{ "remove": ["value1"] }` - **Set** (replace all values): `{ "set": ["value1", "value2"] }` - **Append + Remove** (do both in one call): `{ "append": ["new"], "remove": ["old"] }` "set" cannot be combined with "append" or "remove". Use "set" alone to replace all values or clear them with `{ "set": [] }`. When creating records, use "set" for initial values: `{ "labels": { "set": ["Enterprise"] } }`. When updating records, choose the operation that matches the user's intent. If the user says "add label X", use "append". If they say "remove label X", use "remove". Before using "set" on an existing record, first query the record's current collection values so you know what you're replacing. "set" overwrites the entire collection; if you use it without checking, you will silently discard existing values the user wants to keep. ## Updating records When updating, you are modifying an existing record, not creating one. Do not re-run create-time logic or re-estimate values. - Only include a field when you have an explicit, new signal its value changed. No new signal for a field means leave it out of the call; do not re-send or re-estimate it. - Before overwriting a field that already has a value, read its current value and change it only if it genuinely changed. - Treat amount/value fields as actuals: set them only to committed, agreed values, never projections or estimates. - Do not move a deal's stage to a winning or final stage, or backward, without a clear signal the change actually happened. - For a calendar-synced meeting, a field sourced from the connected calendar event (e.g. title, start, participants) cannot be updated here; the tool returns an error naming the blocked field. Custom fields and any field not sourced from the calendar can still be updated. Ad-hoc meetings created in Clarify have no restricted fields. - Meetings support `summary` (markdown string for meeting summary/recap) and `notes` (markdown string for meeting notes or transcript). Pass these as plain markdown strings in attributes. These can be set on any meeting (ad-hoc or calendar-synced). - Attachments cannot be created here; a file must be uploaded through the app's attachment upload flow. You can update an existing attachment by `id` (e.g. to set custom fields), but its system fields (`s3_key`, `source`, `status`, and its record links) are read-only and will be rejected. ## Upsert mode (match_on) When creating records that may already exist, pass `match_on` with a unique field to match on (marked unique in the schema, e.g. `domains` for companies, `email_addresses` for people) to upsert instead of failing on duplicates. For each record, if exactly one existing record already has the same value for that field, the incoming attributes are merged into it and its ID is returned; records with no match are created. A value that matches multiple existing records errors; merge those records first. Scalar unique fields match exactly; collection fields match case-insensitively; non-unique fields are rejected. The whole batch still succeeds atomically. This avoids the re-query-and-retry dance after a duplicate error. `match_on` only applies to creates; do not combine it with records that have an `id`. Collection field operations (`set`/`append`/`remove`) are honored against the matched record, not flattened into a plain replace. ## Important notes - Rich-text fields shown as `markdown` in the write schema (for example a task's `description`, a deal's `summary`, or a meeting's `summary`) accept a plain markdown string in `attributes` and are converted to rich text automatically. Never open any rich-text/markdown field (`summary`, `prep`, `notes`, `description`, and any custom one) with a heading that restates or mirrors the field name; the field is already labeled in the UI; start directly with the content. - When setting numeric values (like deal amount), store the exact value the user provides. Never multiply or divide to convert between units (e.g. MRR to ARR). - Relationship fields (like `company_id`, `owner_id`, `people`) must be passed via `relationships`, not inside `attributes`. Use `attributes` only for scalar/non-relationship fields. - All relationships are optional. Records like tasks, deals, and people can be created or updated without linking to other records. Never require the user to provide a related record (e.g., a deal) before creating or updating a task. - When the user references a person or company that doesn't exist in the CRM, create the person/company record first using this tool, then link it to the main record. Do not ask the user to manually create the contact or provide unrelated context like a deal. - If you need to process more than 25 records, call this tool multiple times with batches of up to 25. - For a single record, use a single-element array. - After creating or updating records: 1. Tell the user what was done in a friendly, conversational way 2. Refer to records by name only. 3. Never include a record URL in prose; the tool result already carries each record's link.

  • Delete Records

    Delete one or more records by their IDs. Supports bulk deletion of up to 25 records per call. ## When to use this tool - When the user asks to delete or remove specific records ## Important notes - This action is permanent and cannot be undone - All relationships involving the deleted records are also removed - Use only if you already have the IDs for the records - If you need to delete more than 25 records, call this tool multiple times with batches of up to 25.

  • Merge Records

    Merge two or more records into a single primary record in Clarify. All data from the source records (fields, relationships, list memberships, notes) will be merged into the primary record. Source records are deleted after merging. Merge up to 50 groups of duplicates in one call. Before calling, confirm with the user which record in each group is the primary.

  • Get Lists

    Get list metadata (saved views) for an entity type, or fetch a single list by ID. Only dynamic lists (membership driven by a SQL query) are returned. Legacy static and default lists are filtered out. ## When to use this tool - Without list_id: discover existing lists, search by name or description, or filter by creator or owner - With list_id: fetch full details for a specific list Supports case-insensitive substring search and filtering by creator (created_by) or owner (owner_id) when no list_id is provided. ## Returns - List name (title) - Description - SQL query - Layout (table, board) - Owner and creator (user name, email, and user ID) - Last edited (when a person last changed the list's query or layout, and who — or "No edits recorded"). This does not track title, emoji, or description edits. ## Pagination - When user asks for "more" results, use the EXACT same search inputs and only change the offset parameter - The tool will tell you the next offset value to use (e.g., "Call this tool again with offset=25 to continue")

  • Create Or Update List

    Create or update a dynamic list: a saved view whose membership is defined by a SQL query. Dynamic lists are the only list type Clarify supports today. ## When to use this tool - To create a new list: omit list_id and provide title + sql - To update an existing list: provide list_id along with the fields to change - When creating a new list, always set an emoji: a single emoji that represents the list's theme (e.g. "💰" for a deals list, "🎯" for priority accounts, "📅" for a meetings list). The emoji goes only in emoji; never prefix title with it, since the list already renders the emoji next to its title When the user is viewing a list and asks to show, filter, narrow, or sort what they see ("show me just X", "filter to Y", "without Z"), update that list: pass its list_id and the full SQL built from its current definition. The update is staged as a draft, so it does not change the live list for anyone until the user applies it. Create a new list (omit list_id) only when the user asks for a new or separate list. Only pass title with a list_id when the user explicitly asks to rename it. A list is the right surface when the user wants a browsable, filterable set of records of one object type. If they want aggregations, charts, or a multi-part deliverable, that is a report or dashboard (`create-or-update-artifact`), not a list. If the user explicitly asked for a list, build it without asking; if they asked for a report but the result is really just a list of records, offer the list and explain the distinction first. ## If the user asks for a "static list" or to manually curate a list Static lists are deprecated. Clarify no longer offers them as a list type, although some legacy static lists may still exist in the workspace. Do not call this tool to create one. Tell the user static lists are no longer supported, then offer the workaround: add a label to the records they want to track, and create a dynamic list that filters on that label. All built-in entities (companies, people, deals) have a built-in `labels` field with color-coded user-defined values, so the user can add or remove the label to control list membership manually. ## SQL restrictions This tool supports a restricted SQL subset. The query is used for both display and counting (via COUNT(*) wrapping), so certain patterns are incompatible: - **SELECT ***: Use explicit, table-qualified column names (e.g., "SELECT person.name, person.email_addresses FROM person") - **DISTINCT ON**: Not compatible with COUNT(*) wrapping. Use GROUP BY for deduplication instead. - **CTEs (WITH ... AS)**: Not supported. Rewrite using JOINs or WHERE clauses. - **Subqueries in FROM**: SELECT ... FROM (subquery) is not supported. Query the table directly with WHERE/JOIN clauses. - **LIMIT / OFFSET**: Not supported. A dynamic list is a continuously-evaluated membership filter, not a ranked snapshot; the LIMIT is dropped during evaluation, so a "top N" query silently matches every qualifying record. To bound a list, narrow membership with a WHERE clause (e.g. a recency window like "meeting.start > NOW() - INTERVAL '30 days'") or have the user add a label and filter on it. ORDER BY is fine; it sets the default sort, not membership. - Column names must be table-qualified (e.g., "person.name" not just "name") - Aggregate functions in ORDER BY or HAVING must also appear in the SELECT clause - **Filtering labels / multi-select fields**: these store JSONB as `{items: string[]}`. Filter membership with the array-overlap operator, e.g. `(person.labels -> 'items') ?| ARRAY['BDR Prospect']`. Do not use the `@>` containment operator; it works in the database but the list UI cannot render or edit a filter built with it. - **Combining AND and OR**: Wrap each OR (or AND) group in parentheses, e.g. `WHERE company.industry = 'SaaS' AND (company.city = 'Boston' OR company.city = 'Austin')`. Never mix AND and OR at one level without parentheses: `a OR b AND c` runs as `a OR (b AND c)` and the list UI cannot edit it. The list UI edits one level of groups only: each group holds two or more plain conditions joined by a single AND or OR, and groups cannot contain other groups. Deeper nesting still runs but shows as an uneditable advanced filter. ## Filtering by the current user To make a list relative to whoever is viewing it (e.g. "my tasks", "deals I own"), compare against current_setting('app.current_user_id', TRUE), which resolves to the viewing user's ID at query time so the list works for every viewer. - Use it directly in a WHERE clause: WHERE task.assignee_id = current_setting('app.current_user_id', TRUE) - Do not hardcode a specific user's ID, and do not use template tokens like {{current_user_id}}; they are not substituted and the list will match nothing. ## Important notes - Only the fields you provide will be updated; omitted fields remain unchanged - The tool validates both the main query and count query to ensure the list will work when opened - Creating a new list publishes it immediately. Updating an existing list (passing list_id with a new sql) does not publish: the change is saved as a draft version and stays not-live until the user applies it. Relay the apply link from the tool response and tell the user to review and apply it with "Save for everyone"; do not tell the user the update is already live. Metadata-only edits (title, description, emoji) apply immediately. - When updating an existing list's SQL, do not rebuild its membership from memory or a session summary. A large unexplained drop in matched records usually means the new query is incomplete (e.g. after a context reset); re-read the list's current definition and reconcile against it instead of treating the new SQL as authoritative. ## Column choice for list views Lists render one row per root record, so every SELECT column must belong to the root entity. Read the `list-docs` context (via read-context) for the column rules: the title alias, showing a to-one relationship (join `<root>$<fk_column>` and select its primary field aliased `<root>$<fk_column>:__object__`, never the bare `*_id`), and why many-to-many columns don't work in a list. After creating or updating a list: 1. Tell the user what was created or updated in a friendly, conversational way 2. Provide a clickable markdown link to open the list (the tool response will include this) 3. If the tool response includes a prefilled Lead Finder link (returned when a new company or person list matches 0 records), relay it: offer it as the way to fill the list, using the exact markdown link from the response 4. Keep your response concise

  • Delete List

    Delete a list (saved view) by its ID. ## When to use this tool - When the user asks to delete or remove a list ## Important notes - This action is permanent and cannot be undone - Default lists cannot be deleted - The last remaining list for an entity cannot be deleted Whether the user may act on something another person owns depends on their role and workspace settings, and the server makes the final decision. Do not refuse or warn about ownership up front: make the call. A `manage-access` read only says who can change sharing, not who can edit, disable, or delete. If the call is rejected, tell the user the error as returned. Do not retry or work around the rejection, and do not state any permission rule the tool output did not give you.