← All posts

Typed CRM reads: search cannot count

v2.15.0 gives agents three structured HubSpot read tools — one per object type — with exact COUNT(*) totals, discoverable facets, explicit pagination, and honest refusals when a field isn't synced.

Vocion Teamvocion-v2.15.0

Ask an agent "how many open deals do we have?" and, until now, the only place it could look was semantic search. Semantic search answers "what was said" — it returns a relevance top-k, capped at 15 hits. Any count built on top of that is an inference dressed as a count: the agent sees a page of the most relevant deals, adds them up, and reports a number that was never a total. v2.15.0 splits the read path. search_knowledge keeps "what was said"; three new typed tools answer "how many" and "which ones" in SQL over the synced HubSpot mirror.

What's new

One tool per object type, so a model choosing a tool by name chooses correctly — the previous single structured tool spanned contacts, deals, and companies under a contacts name.

ToolAnswersFilters and facets
get_hubspot_contactsPeople — "how many MQLs?", "is X in the CRM?"lifecycle stage
get_hubspot_dealsPipeline — counts, open vs closed, deal valuestage, pipeline, status: open/closed
get_hubspot_companiesAccounts — industry, domain, sizeindustry

Every response leads with an exact total from COUNT(*), independent of the page. Facets are computed before value filters, so the real filter values stay discoverable — the agent reads facets.lifecycleStage to learn that the stage is spelled salesqualifiedlead instead of guessing. Pagination is explicit, with a next_offset and a reminder to report the total, never the page size. Deals also return total_amount and per-stage facet_amounts, so pipeline value by stage is one call — no paging through records adding amounts up.

Two refusal paths matter as much as the answers. A filter value absent from the data returns no count at all — only the value it couldn't find and the real distribution — because returning 0 for a mistyped stage is how a wrong number gets reported as fact. And any field the mirror doesn't carry is named in unavailable_fields, so a missing column produces an honest "that field isn't synced" instead of a plausible number.

Why we built it

There is deliberately no live-API read: the mirror is the single source of truth, because two read paths means two answers to one question. Open vs closed is resolved from HubSpot's pipeline definitions, not stage names — custom pipelines use opaque numeric stage ids, so name-matching heuristics get it wrong. And the tools are source-gated, not grant-gated: they build for any agent whose connected sources include HubSpot, so an agent without CRM access has no CRM tools to hallucinate a call to. The grant gate stays where the privacy guarantee lives, on transcript reading.

Where to find it

Pull v2.15.0 and run migrations 00540055 (partial indexes that keep every call under 55ms on a real mirror). The tools appear automatically for HubSpot-connected agents. And the very next release, v2.16.0 — already covered in Connect to Vocion over MCP — serves these same typed reads over MCP, so any MCP client can call them under the same gates.