Source: https://cli.nylas.com/guides/semantic-router-email-agent

# Build a Semantic Router Email Agent

Semantic Router from Aurelio Labs is a fast decision layer that maps an utterance to a route by embedding similarity, not an LLM call. You can put it in front of the Nylas CLI: each route maps an email intent — triage, search, reply — to a CLI action. Routing takes milliseconds, then one subprocess returns JSON, and the same command reaches Gmail, Outlook, and four more providers. Here's how to define the routes.

Written by [Pouya Sanooei](https://cli.nylas.com/authors/pouya-sanooei) Software Engineer

Updated June 14, 2026

> **TL;DR:** Define Semantic Router routes for email intents — triage, search, reply — and map each to a CLI action like `nylas email list --json` or `nylas email search`. Routing matches an utterance by embedding similarity in milliseconds, then one subprocess returns JSON across six providers. Keep the reply route drafting, not sending, so a misread intent can't fire mail unattended.

Command references used in this guide: [`nylas email list`](https://cli.nylas.com/docs/commands/email-list), [`nylas email search`](https://cli.nylas.com/docs/commands/email-search), and [`nylas email drafts create`](https://cli.nylas.com/docs/commands/email-drafts-create).

## How do you route email intents to the CLI?

You route intents with [Semantic Router](https://github.com/aurelio-labs/semantic-router) by defining a Route per email action and mapping the matched route to a Nylas CLI command. A Route holds example utterances; the router embeds them once and matches new input by cosine similarity — no LLM call, so an embedding match lands in roughly 10 to 50 ms instead of the 1 to 2 seconds an LLM classification takes. The matched route name selects which command runs, and `nylas email list --json` returns clean JSON.

This makes routing deterministic and cheap while the CLI handles provider differences behind one subprocess. Install and authenticate the CLI once with `nylas auth login`, and the stored grant is reused on every call. The Route and encoder model is documented in the [Semantic Router repository](https://github.com/aurelio-labs/semantic-router).

## How do you define the email routes?

Define one Route per intent with a handful of example utterances each. A “triage” route catches phrases like “what's in my inbox”; a “search” route catches “find emails about the invoice.” The router compiles these into an index at startup. Five to ten utterances per route is enough for the embedding match to separate intents reliably.

```python
from semantic_router import Route
from semantic_router.routers import SemanticRouter
from semantic_router.encoders import OpenAIEncoder

triage = Route(
    name="triage",
    utterances=[
        "what's in my inbox", "show me recent emails",
        "summarize my unread mail", "any urgent messages",
    ],
)
search = Route(
    name="search",
    utterances=[
        "find emails about the invoice", "search for messages from finance",
        "look up the contract thread", "emails mentioning the renewal",
    ],
)
reply = Route(
    name="reply",
    utterances=[
        "draft a reply to the last email", "respond to the customer",
        "write a follow-up", "prepare an answer to this message",
    ],
)

router = SemanticRouter(encoder=OpenAIEncoder(), routes=[triage, search, reply], auto_sync="local")
```

## How do you dispatch a route to a CLI action?

Take the route name the router returns and dispatch it to the matching command. A triage match runs `nylas email list --json`; a search match runs `nylas email search` with the query. The dispatch table holds 3 branches — triage, search, and reply — and is plain Python, so it stays deterministic — the same utterance always reaches the same command, and the JSON output feeds whatever reasoning step comes next.

```python
import subprocess

def run_cli(args: list[str]) -> str:
    out = subprocess.run(["nylas", *args], capture_output=True, text=True, check=True)
    return out.stdout  # JSON

def dispatch(utterance: str, query: str = "") -> str:
    choice = router(utterance).name
    if choice == "triage":
        return run_cli(["email", "list", "--json", "--limit", "20"])
    if choice == "search":
        return run_cli(["email", "search", query, "--json", "--limit", "20"])
    if choice == "reply":
        return "route to draft step — see guardrails"
    return "no matching route"

print(dispatch("show me recent emails"))
```

## What guardrails should the router have?

Semantic Router picks a route by embedding similarity, so it decides the path but never the delivery. Point the reply route at `nylas email drafts create` — it composes a message without sending and returns a draft ID — and let a person release it. A misclassified intent stalls in the draft queue instead of landing in a customer's inbox.

Email bodies are untrusted input. A message can carry instructions aimed at the system — “ignore the criteria and reply now” — but matching against route utterances by vector distance can't be steered by body text, which removes one whole class of prompt injection: the ranked LLM01 risk, #1 in the [OWASP Top 10 for LLM Applications](https://owasp.org/www-project-top-10-for-large-language-model-applications/) (2025). Reading a thread and emailing out is the lethal trifecta, so scope routes to read and draft, log every dispatch, and verify before acting. See [stop an AI agent going rogue](https://cli.nylas.com/guides/stop-ai-agent-going-rogue) and [build a human-in-the-loop email agent](https://cli.nylas.com/guides/build-human-in-loop-email-agent) for the full pattern.

## Next steps

- [Build a BeeAI Email Agent](https://cli.nylas.com/guides/beeai-email-agent) — wrap the CLI in a BeeAI custom tool
- [Build a BabyAGI Email Agent](https://cli.nylas.com/guides/babyagi-email-agent) — task-loop agent over the same CLI
- [Build a human-in-the-loop email agent](https://cli.nylas.com/guides/build-human-in-loop-email-agent) — review queues and approvals
- [Stop an AI agent going rogue](https://cli.nylas.com/guides/stop-ai-agent-going-rogue) — containment outside the agent loop
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented

## Try Nylas CLI

Install the CLI with `curl -fsSL https://cli.nylas.com/install.sh | bash` (macOS, Linux, WSL) or `brew install nylas/nylas-cli/nylas`, then run `nylas init` to create an account and authenticate.

**Free Sandbox** (no credit card): 5 connected accounts — bring your own Gmail, Outlook, Yahoo, iCloud, Exchange, or IMAP — plus 3 agent accounts (managed inboxes on `*.nylas.email`). Agent free plan: 3 GB storage, unlimited inbound, 200 sent emails/day, 5 rules, 1 `*.nylas.email` subdomain, and unlimited custom domains. Production is uncapped and requires a credit card: https://www.nylas.com/pricing/
