Guide

Build an AutoGen Email Agent

AutoGen builds conversations between agents, where one agent proposes a tool call and another executes it. To give that loop email, you register a function the agents can call. Wrapping the Nylas CLI keeps the function trivial: it shells out, returns JSON, and reaches six providers without a single line of OAuth. Here's how to register the function and wire the two-agent execution pattern that actually runs it.

Written by Prem Keshari Senior SRE

VerifiedCLI 3.1.16 · Gmail · last tested June 8, 2026

Command references used in this guide: nylas email list, nylas email search, and nylas email drafts create.

How do you give an AutoGen agent email?

You give an AutoGen agent email by registering a function it can call, then letting a second agent execute that function. AutoGen separates proposing a tool call from running it: an assistant agent decides “list the inbox,” and a user-proxy agent actually runs the registered Python. Wrapping the Nylas CLI makes the registered function a few lines — run the command, return stdout.

Because nylas email list --json returns structured data, the executing agent feeds clean JSON back into the conversation for the assistant to reason over. The CLI must be installed and authenticated once with nylas auth login; the grant is reused across runs. AutoGen's function-calling model is documented at microsoft.github.io/autogen.

How do you register the email function?

Write the function, then register it for both the LLM (so the assistant knows it exists) and the executor (so the user-proxy can run it). Keep the body thin: run the command, capture stdout, return it. A narrow function — read or search, not “do anything with email” — is easier for the model to use correctly and easier for you to audit.

import subprocess
from autogen import AssistantAgent, UserProxyAgent

def read_inbox(limit: int = 10) -> str:
    """List recent emails as JSON."""
    out = subprocess.run(
        ["nylas", "email", "list", "--json", "--limit", str(limit)],
        capture_output=True, text=True, check=True,
    )
    return out.stdout

assistant = AssistantAgent("assistant", llm_config={"config_list": config_list})
user_proxy = UserProxyAgent("user_proxy", human_input_mode="NEVER", code_execution_config=False)

# Expose to the model, and let the proxy execute it
assistant.register_for_llm(name="read_inbox", description="List recent emails")(read_inbox)
user_proxy.register_for_execution(name="read_inbox")(read_inbox)

How does the two-agent pattern run it?

Start a chat between the two agents with a task. The assistant reads the goal, emits a function call for read_inbox, and the user-proxy executes it and returns the JSON. The assistant then reasons over the result and replies — for example, summarizing unread mail by urgency. This propose-then-execute split is AutoGen's core pattern, and it keeps tool execution in one controllable place.

user_proxy.initiate_chat(
    assistant,
    message="Read my 15 most recent emails and tell me which three need a reply today. "
            "Do not send anything — just summarize.",
)
# assistant -> proposes read_inbox(limit=15)
# user_proxy -> runs the CLI, returns JSON
# assistant -> summarizes the three that need a reply

Why use the CLI as the function body?

The CLI collapses provider differences into one tool. Without it, an AutoGen email function means choosing a provider SDK, handling its OAuth, and writing serialization — repeated per provider. With it, the same registered function reaches Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP, because the connected grant determines the backend, not your code. Adding a provider is a login, not a new function.

Uniform JSON is the second benefit: every command returns the same shape regardless of provider, so the assistant's reasoning doesn't branch on where the mail lives. That keeps the registered functions small and the conversation logic provider-agnostic, which is exactly what you want when the model, not your code, is choosing actions.

What guardrails should the agent have?

Don't register a send function. Register a draft function that runs nylas email drafts create — it composes without sending and returns a draft ID a person reviews before it goes out. With human_input_mode set to require approval on consequential turns, the agent can prepare mail but never dispatch it unattended, so a misread thread can't reach a real recipient.

Treat every email body as untrusted: a message may carry instructions aimed at the agent, and the agent must not act on content as if it were a command. Scope the conversation to read and draft, log each tool call, and verify before any send. See stop an AI agent going rogue and email prompt injection defense for the containment patterns.

Next steps