Guide

Build a MetaGPT Email Agent

Give MetaGPT Roles email Actions through the Nylas CLI: JSON inbox reads, draft replies, SOP boundaries, message-pool handoffs, and six-provider support.

Written by Caleb Geene Director, Site Reliability Engineering

VerifiedCLI 3.1.20 · Gmail · last tested June 14, 2026

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

How do you give a MetaGPT agent email?

A MetaGPT email agent works best when inbox access is modeled as a narrow Action that a Role can run during its SOP. MetaGPT's official project describes agents as a software company, with Role objects such as ProductManager and Engineer carrying out Action units while exchanging messages. Email becomes 1 Action in that company.

The pattern is to authenticate once with nylas auth login, then call email commands from inside a Role's Action. That keeps provider auth out of MetaGPT and lets the same Role handle Gmail, Microsoft, and 4 other providers. Reuse the existing official MetaGPT links for reference: GitHub and docs.deepwisdom.ai.

How does a MetaGPT Role run an email Action?

A MetaGPT Role runs email by owning an Action whose run method starts one subprocess and returns JSON to the Role. That maps cleanly to MetaGPT's software-company metaphor: the Support Role does triage, the Replier Role drafts, and neither Role needs provider SDK objects. One Action should do exactly 1 inbox operation.

The command here is nylas email list --json --limit 10. Run it when the Support Role begins a triage cycle, because it gives the newest 10 messages as JSON in one terminal call. MetaGPT Actions are async, so asyncio.create_subprocess_exec keeps the Role loop from blocking while the network request completes.

import asyncio
from metagpt.actions import Action

class ReadInbox(Action):
    name: str = "ReadInbox"

    async def run(self, limit: int = 10) -> str:
        proc = await asyncio.create_subprocess_exec(
            "nylas", "email", "list", "--json", "--limit", str(limit),
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await proc.communicate()
        if proc.returncode != 0:
            raise RuntimeError(stderr.decode())
        return stdout.decode()

Assign this Action only to Roles that need inbox visibility. A ProductManager Role might read summaries from the shared message pool, while the Support Role performs the direct inbox read. That split mirrors a 2-person support desk instead of giving every Role the same mailbox permission.

How do Roles share triage through the message pool?

MetaGPT Roles coordinate through shared messages, so the Support Role should publish triage facts instead of handing raw mailbox output to every participant. The useful unit is a small message with sender, subject, id, urgency, and recommended next Action. That keeps a 20-message inbox scan from becoming shared prompt clutter.

The command here is nylas email search --json --limit 20 --unread. Run it when the Support Role needs a focused unread queue before posting to MetaGPT's message pool. The jq step extracts 4 fields, which is enough for routing without copying full email bodies into every Role context.

nylas email search "refund OR escalation" --json --limit 20 --unread \
  | jq '[.[] | {id, from, subject, date}]'

After the search, the Support Role can send a compact message such as “3 unread refund threads need drafts; thread ids attached.” A Replier Role watches for that message type and runs the drafting Action only for the selected ids. The message pool becomes the handoff channel, not a mailbox mirror.

How does the agent call email commands?

The agent calls email commands by treating the CLI as a process boundary with JSON on stdout and errors on stderr. That boundary is useful in MetaGPT because each Action can validate inputs before spawning a command, then return a plain string that the Role can summarize, classify, or publish. Keep each call under 1 responsibility.

The command here is nylas email read MESSAGE_ID, where the id is a positional argument. Run it after a triage Action has chosen a specific message id, because reading 1 thread gives the Replier Role enough context to draft without scanning the whole inbox again. The same wrapper can also capture nonzero exits before MetaGPT continues its SOP.

async def read_message(message_id: str) -> str:
    proc = await asyncio.create_subprocess_exec(
        "nylas", "email", "read", message_id, "--json",
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    stdout, stderr = await proc.communicate()
    if proc.returncode != 0:
        raise RuntimeError(stderr.decode())
    return stdout.decode()

Keep the command array literal, not shell-concatenated text, so message ids can't alter the command. The Role should pass only the id chosen during triage. If the Action needs sender or subject, parse it from the JSON result instead of adding another broad inbox read.

How do SOPs separate triage from drafting?

MetaGPT's SOP idea is the reason this guide should not build one all-powerful email Role. Give Support the read/search Actions, give Replier the draft Action, and let messages describe the handoff. That is closer to a 3-step operating procedure: inspect, decide, draft. Delivery stays outside the autonomous loop.

The command here is nylas email drafts create. Run it only after the Replier Role has received a specific thread id and proposed reply body, because drafts create reviewable mail without delivering it. The example includes --reply-to so the draft stays attached to the original conversation.

nylas email drafts create \
  --to "customer@example.com" \
  --subject "Re: Refund request" \
  --body "Thanks for writing in. I found your order and drafted the next step for review." \
  --reply-to "message_123" \
  --json

The Action should return the draft id to the message pool, not the final email. A reviewer, dashboard, or separate approval workflow can inspect that id. This keeps MetaGPT's productive multi-agent flow while stopping a Role from converting a bad classification into an external message.

What guardrails should the agent have?

A MetaGPT email agent needs guardrails at the Action boundary, Role assignment, and SOP handoff. The risky case is the lethal trifecta: private data + untrusted content + external communication. Email supplies all 3, and OWASP LLM01 (2025) names prompt injection as the top LLM application risk.

Keep outbound mail behind nylas email drafts create, not nylas email send, for autonomous loops. That gives a human at least 1 review point before an external customer sees a reply. Also keep read Actions and draft Actions on different Roles, so one prompt-injected email body can't both choose a target and send content.

Add simple limits before the subprocess call: cap list/search at 20 messages, require a message id for reads, reject unknown recipients, and log every draft id. For deeper containment, pair this with stop an AI agent going rogue and build a human-in-the-loop email agent.

Next steps

Use these 5 follow-ups to compare MetaGPT with other agent styles, then verify any new command syntax in the reference before adding it to an Action.