Skip to content

Fresh Contacts Tool

The get_fresh_contacts tool lets users ask the assistant for contacts that have never received an outbound message from the organization. It helps sales teams identify leads needing initial outreach.

When a user asks something like “Show me contacts I haven’t reached out to yet”, the assistant invokes get_fresh_contacts. The tool:

  1. Checks Message::Collection::List permission
  2. Parses the time range (defaults to all_time)
  3. Queries contacts with zero outbound messages using a NOT EXISTS subquery
  4. Batch-fetches preferred phone numbers
  5. Formats timestamps in the organization’s timezone
  6. Injects deep links so each result is clickable
{
time_range?: "today" | "yesterday" | "last_3_days" | "last_7_days"
| "last_14_days" | "last_30_days" | "last_90_days" | "all_time"
// Filters by contact creation date. Defaults to "all_time".
}
{
contacts: [
{
contact_id: "uuid",
first_name: "Jane",
last_name: "Doe",
contact_phone: "+15551234567", // preferred phone, or null
created_at: "2026-04-01 09:30 AM", // formatted in org timezone
deep_link: "/contacts/{id}"
}
],
count: 25, // items on this page
total_count: 142 // total matching contacts
}

The tool uses two-layer permission enforcement:

LayerPermissionEffect
RegistryMessage::Collection::ListControls whether the tool appears
ServiceContact::Collection::ListFull org access
ServiceContact::Collection::ListAssignedScoped to assigned contacts only

Users with ListAssigned only see fresh contacts assigned to them. The service adds a member-scoping filter automatically.

The service uses raw SQL with a NOT EXISTS pattern because SeaORM cannot express subquery existence checks:

SELECT c.id, c.first_name, c.last_name, c.created_at,
COUNT(*) OVER() AS total_count
FROM contact c
WHERE c.organization_id = $1
AND NOT EXISTS (
SELECT 1 FROM message m
WHERE m.contact_id = c.id AND m.direction = 'outbound'
)
ORDER BY c.created_at DESC
LIMIT $2 OFFSET $3

Optional WHERE clauses are added for time range filtering (c.created_at >= $start) and member scoping (c.id IN (SELECT contact_id FROM contact_user WHERE member_id = $member)).

Phone numbers are batch-fetched in a single query to avoid N+1 issues, with is_preferred = true phones taking priority.

PR #766 also extracted parse_assistant_time_range() into tools/time_range.rs as a shared utility. Both get_fresh_contacts and get_unanswered_contacts use it.

src/mods/assistant/tools/time_range.rs
pub(crate) fn parse_assistant_time_range(
value: &str,
) -> (Option<NaiveDateTime>, Option<NaiveDateTime>)

The function returns (start, end) as UTC midnight boundaries. "all_time" returns (None, None).

FilePurpose
assistant/tools/messages/ai_get_fresh_contacts_tool.rsTool definition and handler
messaging/services/get_fresh_contacts_service.rsService with raw SQL query
messaging/types/fresh_contact_type.rsFreshContactsFilter, FreshContact, FreshContactsResponse
assistant/tools/time_range.rsShared parse_assistant_time_range() utility
assistant/types/assistant_tool_name_type.rsGetFreshContacts enum variant