Kvika Team·· 8 min read

Resolving Colleagues by Name, Not Email

Users say 'email Sarah' — not sarah@company.com. A three-tier directory lookup across Teams, past calendar attendees, and org search, with fuzzy scoring and graceful degradation when admin consent blocks directory APIs.

microsoft-teamsemailschedulingengineeringux

Natural language breaks the moment your compose form requires an email address. Users say "schedule with Kinshuok" or "email Sarah about the review." An AI agent passes those strings through. If your backend rejects anything without an @ symbol, the agent asks the user for an address — and the magic dies.

Name resolution has to happen server-side with the user's OAuth token, not in the model. We built a tiered directory in Kvika that resolves names to emails for compose, meeting prep, and typeahead pickers — without requiring admin-consented directory scopes on every tenant.

Why email addresses break the flow

OpenAPI schemas that mark the to field as format: email cause Bedrock action groups to reject valid name inputs before your code runs. Remove that constraint. Validate on the server after resolution.

The agent prompt explicitly says: pass raw names; Kvika resolves them. Compose and meeting prep share one function — resolveAttendees — so behaviour is consistent across email drafts and scheduler cards.

Three tiers of lookup

For each attendee string, the resolver runs in order:

1. Pass-through. If the string matches an email regex, use it directly.

2. Local directory. Merge colleagues from Microsoft Teams (up to five joined teams, members fetched with the user's token) plus contacts mined from calendar history. Score every candidate against the query.

3. Live org search. If local score is below threshold, query Graph: /users?$search="displayName:name" with ConsistencyLevel: eventual. If $search is denied, fall back to startswith(displayName, ...) and /me/people.

typescript
for (const query of attendees) {
  if (isEmail(query)) return { email: query, matched: true }

  const local = bestScore(localDirectory, query)
  if (local.score >= 50) return { email: local.email, matched: true }

  const org = await searchOrgDirectory(accessToken, query)
  if (org) return { email: org.email, matched: true }

  return { email: null, matched: false } // UI must let user fix
}

Contacts from past meetings

Teams directory APIs fail in tenants without admin consent for User.ReadBasic.All. Calendar read often already exists. We build a secondary directory from the last 400 synced calendar events: extract attendee JSON, dedupe by email, keep display names.

These are people the user has actually met with — high precision for scheduling and compose. New hires with no meeting history will not appear here until the first invite; that is when org search or manual picker kicks in.

This needs no extra Graph scopes beyond calendar data you already store. It is the best fallback when directory search is blocked.

Fuzzy scoring and thresholds

Exact string match is not enough. "Sarah" must match "Sarah Chen." We tokenise display names and score:

Full string equal → 100. Exact token match → 90. Token prefix → 70. Substring → 50. Email local-part match → 95/75. Accept at ≥ 50; below that, return matched: false and show a person picker.

Common first names at score 50 can wrong-match. Low confidence must never auto-send email. The compose UI uses the same searchPeople typeahead as the meeting scheduler — parallel fetch of all three sources, dedupe by email, return top eight by score.

When resolution fails

No Microsoft integration. Teams and org search unavailable; calendar contacts may still work from Google-sourced events if attendees were stored on sync.

Expired client secret. Token refresh fails; directory calls return empty. Surface reconnect — not "person not found."

Agent emits bracket syntax. Models sometimes pass [aakarsh] as an attendee. Strip brackets and quotes before resolution in meeting draft normalisation.

Related: mixed-stack scheduling · findMeetingTimes fallback. Join the Kvika beta.

Kvika Team

Kvika unifies your calendar, email, and tasks across Google and Microsoft. Join the beta at kvika.work/waitlist.