Guide

Give Your AI Agent a Contacts Directory

An AI agent that sends email needs to know who it's writing to. The agent account that carries the agent's email and calendar also carries contacts — a directory the agent owns and queries from the CLI. This guide adds contacts, resolves a name to an address before a send, groups recipients for batch outreach, dedupes the list, and turns the directory into the source of truth for who the agent is allowed to contact. One grant, no separate CRM integration.

Written by Nick Barraclough Product Manager

VerifiedCLI 3.1.16 · Nylas managed · last tested June 7, 2026

Why does an AI agent need its own contacts directory?

An agent that sends email has to answer a question on every message: what address does this name map to? A contacts directory on the agent account answers it. The grant that holds the agent's inbox and calendar also holds a contacts store the agent reads and writes from the CLI — no separate CRM API, no second set of credentials, no provider-specific contact schema to parse.

Owning the directory matters for the same reason owning the inbox does. The agent's contacts are scoped to the agent's grant, so deleting the grant removes them, and the directory becomes a clean source of truth for who the agent is supposed to contact. That last point turns contacts into a guardrail, not just a convenience — the directory defines the allow-list the agent works within.

Agent contacts directory feeds three jobs: resolve a name to an address, group recipients, and check an allow-listContacts directoryon the agent account grantResolvename → addressGrouprecipient listsGuardallow-list source

How do I add contacts to the agent's directory?

Populate the directory with nylas contacts create. The command writes a structured contact to the agent's grant with separate fields for first name, last name, email, company, job title, and phone — the shape that downstream lookups and personalization depend on. Splitting the name into --first-name and --last-name is what lets a later greeting say "Hi Jane" without string-splitting a full name.

nylas contacts create \
  --first-name "Jane" \
  --last-name "Smith" \
  --email "jane@acme.com" \
  --company "Acme" \
  --job-title "VP Operations"

For bulk loading, drive the same command from a shell loop over a CSV — the agent's directory fills in seconds rather than through a paginated web UI one record at a time. The fields follow the vCard standard (RFC 6350), so an export from another address book maps cleanly onto the create flags.

How does the agent resolve a name to an email address?

Before a send, the agent turns a name into an address with nylas contacts search. The command filters on real fields — --query, --company, --email, and --has-email — and returns up to 50 matches by default. Resolving the address from the directory keeps a language model from guessing an email that bounces.

nylas contacts search --query "Jane Smith" --has-email --json \
  | jq -re '.[0].emails[0].email // empty'

The -r flag emits the raw address (not a quoted JSON string) and -e makes jq exit non-zero when there's no match, so the agent fails loudly on an unknown name instead of piping an empty string into a send. This is the deterministic guard between the model's intent ("email Jane") and the actual address — the lookup is code, not a guess. Pass --company "Acme" when two people share a name and you need to disambiguate.

How does the agent group recipients?

For batch outreach, the agent organizes contacts with nylas contacts groups. Create a group, then query its members with the --group filter on search. A group is a stable handle — "Q3 webinar invitees" — that the agent resolves to a current recipient list at send time, instead of hard-coding addresses in a script.

nylas contacts groups list --json | jq '.[] | {id, name}'

nylas contacts search --group grp_abc123 --has-email --json \
  | jq -r '.[].emails[0].email'

Piping the group's addresses into a send loop is how the agent runs a campaign without a marketing tool. Each address comes from the directory, so the recipient list reflects the current group membership — add a contact to the group today and tomorrow's run picks it up. For the personalization layer on top of this, see personalizing outbound email from the CLI.

How does the agent keep the directory clean?

A directory the agent writes to drifts toward duplicates, so dedup is a maintenance job worth scripting. Pull a page with nylas contacts list --json and group by email to find repeats. The --limit flag raises the page size so a single scan covers up to 500 contacts — directories larger than that need pagination across multiple calls. The filter drops contacts with no email before grouping, since a null key isn't a real duplicate:

nylas contacts list --json --limit 500 \
  | jq '[ .[] | select((.emails | length) > 0) ]
       | [group_by(.emails[0].email)[] | select(length > 1)
       | {email: .[0].emails[0].email, count: length}]'

Catching duplicates before a batch send is what stops the agent from emailing the same person twice in one run. Run the scan on a schedule and the directory stays trustworthy as the source of recipient lists. The same --json output drives a CSV export with jq's @csv filter when you need the directory outside the agent — see managing contacts from the terminal for the export and update patterns.

How does the directory power an allow-list guardrail?

The directory's second job is containment. The contacts store is the allow-list the agent works within, and an outbound rule on the agent's workspace enforces a hard boundary around it. No rule primitive checks contact membership directly — rules match on envelope fields like the recipient domain — so the rule below enforces a company-domain boundary, and your send loop reconciles individual recipients against the directory. The rule is evaluated before the message reaches the send pipeline, so a prompt injection can't talk the agent past it — the constraint lives outside the agent's decision loop.

# Block any outbound mail to a recipient outside the company domain
nylas agent rule create \
  --name "Restrict outbound to company domain" \
  --trigger outbound \
  --condition recipient.domain,is_not,acme.com \
  --action block

That single-domain rule fits an internal agent. For an agent that contacts customers across many domains, derive the allowed set from the directory — extract recipient domains from contacts list --json and reconcile sends against it in your loop, then layer the workspace rules for the hard stops. The full set of triggers, conditions, and actions is in Agent Rules and Policies, and the containment pattern is in Stop Your AI Agent From Going Rogue.

Contacts directory vs a CRM or database

An agent-account contacts directory isn't a CRM replacement — it's the working set the agent needs at send time. The difference is scope: the directory answers "who is this and may I email them," while a CRM tracks deals, history, and reporting. For most agents the directory is enough, and it ships on the grant the agent already has.

ConcernExternal CRM / databaseAgent account contacts
SetupSeparate API keys and schemaOn the agent's existing grant
LookupSQL or a vendor SDKOne contacts search call
ScopeOrg-wide system of recordScoped to one agent grant
TeardownDelete rows, manage retentionDelete the grant

When the agent does need deal history, export the directory and sync it outward — the --json output feeds any CRM import. Both the directory and the connected providers speak the same Nylas v3 Contacts API, so a script written against one works against the other.

Next steps