Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesPeople SearchBehavior — people search

Behavior — people search

PeopleSearchService.search(orgId, clerkId, query) (apps/api/src/people-search/) is the one implementation. Spotter reaches it through read.people_search and through the member/lead scopes of read.search_anything, so both see exactly the same people. Result shape: { members: PeopleSearchMemberHit[], leads: PeopleSearchLeadHit[] } (libs/shared/src/lib/schemas/people-search.schema.ts); the tool returns { items: [...members, ...leads], totalCount }.

Who sees what

CallerMembers & staffLeads
Owner / adminallall
Coach with a team-scope Sales grantallall
Coach with an own-scope Sales grantallonly leads assigned to them
Coach without Sales accessallnone
Member403403

Lead visibility is SalesWorkspaceAccessService.actor — the same rule Tasks uses for lead-linked tasks.

Scope

No list filters.

  • Members & staff: every memberships row of the org, any role, any status. Removed memberships and deleted users are excluded. Managed children are found by name; they are never embedded.
  • Leads: every organization_leads row of the org, archived included (archived: true).

Matching

A query shorter than 2 characters returns nothing; the tool rejects one longer than 100.

A person matches when ANY of:

  1. Text — every whitespace/comma term appears (case-insensitive) in some name or email column. “Levi Dana” finds Dana Levi across first/last name.
  2. Phone — the query is phone-shaped (digits, spaces, + ( ) . -) with at least 3 digits. A complete number reduces to its national significant number (050-123-4567 and +972 50 123 4567 → 501234567); a partial one drops its trunk zeros (050-12 → 5012). The digits are matched inside the stored phone’s digits, so E.164 and legacy formatted phones both match.
  3. Similar name — PersonNameEmbeddingService candidates, the same retrieval the members list, leads list and inbox use. Asked only when the query has a letter and no @. If Voyage or the vector query fails, search degrades to text + phone.

Each hit carries match: phone, email, name or similar_name. Literal matches rank first, similar_name after, by display name within each. At most 8 per group, chosen from the first 24 rows by name.

Spotter lanes

  • Direct (no model): the conversation’s first message, trailing ?!.,;: dropped, is only an email (x@y, no spaces) or a complete, valid phone number starting with 0 or + (normalizePhone) → directPeopleQuery routes it to read.people_search. Dates, amounts and partial numbers take the router. Later messages never take this lane: a number may be the answer to Spotter’s own question.
  • Router (one Haiku call): anything else goes to the existing T1 router; a name or “find Dana” picks read.people_search.
  • Card-only: read.people_search is quickAnswer: 'card', so the quick route skips the verbalizing call. The web card (PeopleResults) renders the whole answer: name, email · phone, role or lead status, “Cancelled”/other non-active member status, “Archived”, and “Similar name” on embedding hits. No hit: “No one matches that.”
  • Title: a card-only turn titles the conversation with the message as typed — no Haiku title call.
  • Picking a row opens /{lang}/dashboard/members/{membershipId} or /{lang}/dashboard/leads?lead={leadId} and closes the Spotter sheet.
  • Over budget: AgentController lets a message through the budget gate when directPeopleQuery matches it (no intent), and passes budgetExceeded to the orchestrator. That run only tries the direct lane — in any conversation, since no model can answer a follow-up anyway — and ends anything else with agent_budget_exceeded, calling no model. The usage footer’s at-cap banner adds “You can still look someone up: type just their phone number or email.”

Rollout

Unflagged: a new read-only lookup that changes no existing flow, and the API enforces the same member/lead visibility as the existing lists. read.search_anything now applies that visibility too: before, it returned removed members and every lead to any staff member. Its people hits follow the search’s rules — at most 8 members and 8 leads whatever limit asks, none for a 1-character query, archived leads included with archived in the snippet — and any people scope runs both groups.