Guide

AI Agent CLI for Email and Calendar

An AI agent CLI turns shell commands into reliable LLM tools. This guide shows how to use the Nylas CLI as your email and calendar tool backend instead of writing OAuth flows and provider-specific API clients. One subprocess call per tool works across Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP.

Written by Pouya Sanooei Software Engineer

Reviewed by Hazik

VerifiedCLI 3.1.1 · Gmail, Outlook · last tested April 11, 2026

What is an AI agent CLI for email and calendar?

An AI agent CLI is a command-line tool that an LLM can call through subprocess wrappers or MCP. For email and calendar tasks, the CLI becomes the tool backend: it lists messages, searches mail, sends replies, reads calendars, creates meetings, and returns JSON the agent can parse.

This pattern keeps the tool boundary small. Instead of writing OAuth clients and provider-specific API code, you define each capability as one CLI call with structured output. The agent decides when to invoke each command based on the user's request.

The tool-use pattern — define tools, let the LLM decide when to call them — is now supported by every major provider: OpenAI's function calling API (June 2023), Anthropic's tool use API (April 2024), and Google's function calling in Gemini (December 2023). The interface differs slightly, but the core loop is identical: send tool definitions, receive tool calls, return results.

Email and calendar are natural fits for this pattern. Your agent needs to read messages, send replies, check availability, create events. A custom Gmail OAuth integration requires roughly 300 lines of boilerplate just for token management. The Nylas CLI eliminates that: run nylas email list or nylas calendar events list from your tool handlers with one subprocess call per tool, and get back JSON covering Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP.

1. Install and authenticate

Authentication is a one-time step that stores your credentials locally so every subsequent CLI call works without prompts. The Nylas CLI stores OAuth tokens in ~/.config/nylas/ and supports 6 providers — Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP — through a single auth flow. After running nylas auth login, verify the connection with nylas auth whoami before wiring tools into your agent.

# Install
brew install nylas/nylas-cli/nylas

# Authenticate (one-time)
nylas auth login

# Verify
nylas auth whoami
nylas email list --limit 3

2. The tool pattern

The tool pattern is a Python function that wraps a CLI command in a subprocess.run() call and returns the stdout as structured JSON. Every agent framework — OpenAI, Anthropic, Google — expects tools as callable functions with typed parameters. Each function below maps to exactly one CLI command, keeping tool implementations under 10 lines each.

import subprocess
import json

def list_emails(limit=10, unread_only=False):
    """List recent emails from the authenticated mailbox."""
    cmd = ["nylas", "email", "list", "--limit", str(limit), "--json"]
    if unread_only:
        cmd.append("--unread")
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        return f"Error: {result.stderr}"
    return result.stdout

def send_email(to, subject, body):
    """Send an email. Requires --yes to skip confirmation."""
    result = subprocess.run(
        ["nylas", "email", "send", "--to", to, "--subject", subject, "--body", body, "--yes"],
        capture_output=True,
        text=True
    )
    if result.returncode != 0:
        return f"Error: {result.stderr}"
    return "Email sent successfully."

When your agent calls list_emails(), the CLI returns structured JSON with sender, subject, snippet, timestamp, and folder data. The --json flag on nylas email list produces an array of message objects. Each object includes a 200-character snippet the LLM can use to decide whether to read the full message.

[
  {
    "id": "a1b2c3d4e5f6g7h8",
    "subject": "Re: API design review",
    "from": [{"name": "Sarah Chen", "email": "sarah@example.com"}],
    "to": [{"name": "Alex Rivera", "email": "alex@example.com"}],
    "snippet": "I've updated the endpoint spec. The breaking change is in the auth middleware...",
    "date": "2026-03-25T14:22:18-04:00",
    "unread": true,
    "folders": ["INBOX"]
  },
  {
    "id": "b2c3d4e5f6g7h8i9",
    "subject": "Deployment complete: staging-v2.4.0",
    "from": [{"name": "CI Bot", "email": "ci@example.com"}],
    "to": [{"name": "Alex Rivera", "email": "alex@example.com"}],
    "snippet": "All 47 tests passed. Deployment to staging completed in 3m 22s.",
    "date": "2026-03-25T13:15:00-04:00",
    "unread": false,
    "folders": ["INBOX"]
  }
]

When send_email() runs nylas email send --yes --json, the CLI returns a confirmation object with the message ID, recipients, and timestamp. The --yes flag skips interactive confirmation, which is required for non-interactive agent loops where stdin isn't available.

{
  "id": "msg_c3d4e5f6g7h8i9j0",
  "subject": "Re: API design review",
  "from": [{"name": "Alex Rivera", "email": "alex@example.com"}],
  "to": [{"name": "Sarah Chen", "email": "sarah@example.com"}],
  "date": "2026-03-25T14:25:00-04:00",
  "object": "message"
}

3. Tool definitions for the LLM

Tool definitions are JSON schemas that tell the LLM what each function does, what parameters it accepts, and which parameters are required. The LLM reads these definitions to decide when and how to call each tool. According to OpenAI's function calling docs, well-written descriptions reduce hallucinated tool calls by up to 40%. The format below follows OpenAI's schema; Anthropic and Google use similar structures.

tools = [
    {
        "type": "function",
        "function": {
            "name": "list_emails",
            "description": "List recent emails from the user's inbox. Use unread_only=True to filter unread only.",
            "parameters": {
                "type": "object",
                "properties": {
                    "limit": {"type": "integer", "description": "Max number of emails to return", "default": 10},
                    "unread_only": {"type": "boolean", "description": "Only return unread emails", "default": False}
                }
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "send_email",
            "description": "Send an email. Use for replies or new messages.",
            "parameters": {
                "type": "object",
                "properties": {
                    "to": {"type": "string", "description": "Recipient email address"},
                    "subject": {"type": "string", "description": "Email subject"},
                    "body": {"type": "string", "description": "Email body (plain text)"}
                },
                "required": ["to", "subject", "body"]
            }
        }
    }
]

4. Wire tools into your agent loop

The agent loop is a while loop that sends messages to the LLM, checks for tool calls in the response, executes those tools, appends the results to the conversation context, and calls the LLM again. This loop repeats until the LLM returns a plain text response with no tool calls. A typical email agent processes 3-5 tool calls per user request — for example, listing emails, reading one, then drafting a reply.

import json
from openai import OpenAI

client = OpenAI()
context = []

def call():
    return client.chat.completions.create(
        model="gpt-4o",
        messages=context,
        tools=tools,
        tool_choice="auto"
    )

def handle_tool_call(item):
    name = item.function.name
    args = json.loads(item.function.arguments or "{}")
    if name == "list_emails":
        result = list_emails(**args)
    elif name == "send_email":
        result = send_email(**args)
    else:
        result = "Unknown tool"
    return {
        "role": "tool",
        "tool_call_id": item.id,
        "content": result
    }

def process(user_input):
    context.append({"role": "user", "content": user_input})
    while True:
        response = call()
        message = response.choices[0].message
        tool_calls = message.tool_calls or []

        if not tool_calls:
            final = message.content or ""
            context.append({"role": "assistant", "content": final})
            return final

        context.append({
            "role": "assistant",
            "content": message.content or "",
            "tool_calls": [
                {
                    "id": tc.id,
                    "type": "function",
                    "function": {
                        "name": tc.function.name,
                        "arguments": tc.function.arguments,
                    },
                }
                for tc in tool_calls
            ],
        })

        for item in tool_calls:
            context.append(handle_tool_call(item))

5. Add calendar tools

Calendar tools follow the same subprocess pattern as email tools. The Nylas CLI exposes 3 calendar operations — listing events, creating events, and finding available meeting times — each as a single command that returns JSON. Calendar data includes participant RSVP status, conferencing links, and timezone information, giving the agent enough context to schedule meetings without follow-up questions.

def list_events(days=7):
    """List upcoming calendar events."""
    result = subprocess.run(
        ["nylas", "calendar", "events", "list", "--days", str(days), "--json"],
        capture_output=True,
        text=True
    )
    return result.stdout if result.returncode == 0 else f"Error: {result.stderr}"

def create_event(title, start, end, participants=None):
    """Create a calendar event."""
    cmd = ["nylas", "calendar", "events", "create", "--title", title, "--start", start, "--end", end]
    if participants:
        for p in participants:
            cmd.extend(["--participant", p])
    result = subprocess.run(cmd, capture_output=True, text=True)
    return result.stdout if result.returncode == 0 else f"Error: {result.stderr}"

def find_meeting_time(participants, duration="30m"):
    """Find when participants are free for a meeting."""
    result = subprocess.run(
        ["nylas", "calendar", "find-time", "--participants", ",".join(participants),
         "--duration", duration, "--json"],
        capture_output=True,
        text=True
    )
    return result.stdout if result.returncode == 0 else f"Error: {result.stderr}"

When list_events() calls nylas calendar events list --json, the output includes participant RSVP status, conferencing provider and join URL, and IANA timezone identifiers. Each event object contains start/end times as Unix timestamps, which the LLM can compare to find conflicts or open slots.

[
  {
    "id": "evt_9x8y7z6w5v4u3t2s",
    "title": "API Design Review",
    "when": {
      "start_time": 1774535400,
      "end_time": 1774537200,
      "start_timezone": "America/New_York",
      "object": "timespan"
    },
    "participants": [
      {"email": "alex@example.com", "status": "yes"},
      {"email": "sarah@example.com", "status": "yes"},
      {"email": "jordan@example.com", "status": "noreply"}
    ],
    "status": "confirmed",
    "conferencing": {
      "provider": "Google Meet",
      "details": {"url": "https://meet.google.com/abc-defg-hij"}
    }
  }
]

6. CLI commands you can wrap

The Nylas CLI provides 7 commands that map directly to agent tools — 4 for email operations and 3 for calendar operations. Each command accepts a --json flag for structured output that LLMs can parse reliably. The tables below list every command, its flags, and the agent use case it covers.

Email

CommandUse case
nylas email list --jsonList messages (add --unread, --limit)
nylas email search "query" --jsonSearch by keyword
nylas email read msg_id --jsonRead full message
nylas email send --to X --subject Y --body Z --yesSend email

Calendar

CommandUse case
nylas calendar events list --jsonList events (add --days, --timezone)
nylas calendar events create --title X --start Y --end ZCreate event
nylas calendar availability find --participants X,Y --duration 30 --jsonFind free slots

7. Context engineering tips

Context engineering is the practice of managing what goes into and out of an LLM's context window to control cost and quality. Each tool call's JSON output counts toward the context window — a single nylas email list --limit 50 call can produce 8,000-12,000 tokens, roughly 10% of GPT-4o's 128K context. Keeping tool outputs compact improves response quality and cuts API costs.

  • Use --limit 5 or --limit 10 instead of fetching everything — 10 messages produce roughly 2,000 tokens
  • Summarize large outputs in a separate LLM call before appending to the main context
  • Only expose the tools the agent needs for the task. Email-only agents don't need calendar tools — fewer tool definitions means fewer tokens per request.

Using Cursor or Claude instead?

The Model Context Protocol (MCP) is an open standard from Anthropic that lets AI assistants call external tools without custom agent code. If you want email and calendar tools inside Claude Code, Cursor, or VS Code, the Nylas MCP server provides the same capabilities as the subprocess tools above — but installs with a single command instead of writing Python wrappers. MCP supports 4 assistants: Claude Code, Cursor, Windsurf, and VS Code.

The install command registers the Nylas MCP server with your chosen assistant's configuration file. After running it, the assistant gains access to email list, send, search, and calendar tools without any Python code.

nylas mcp install --assistant claude-code
# or: cursor, windsurf, vscode

See Give AI Agents Email Access via MCP for the full setup walkthrough.

FAQ

These are the most common questions developers ask when wiring CLI-based email and calendar tools into LLM agents. The answers cover provider compatibility, authentication in headless environments, and multi-grant setups — the 3 areas where agent builds most often stall.

Does this work with Anthropic, Gemini, or other LLM providers?

Yes. The tool pattern (define tools, handle tool calls, append results to context) is the same across all 3 major providers. Swap the client.chat.completions.create call for your provider's equivalent. The CLI subprocess wrappers don't change — they return JSON regardless of which LLM consumes it.

What if the CLI is not in PATH?

Use the full path to the binary (e.g. /opt/homebrew/bin/nylas on macOS) or pass shell=True with the full command string. For Homebrew installs, which nylas shows the path.

How do I use a specific mailbox when I have multiple grants?

Set NYLAS_GRANT_ID in the environment before running your agent, or pass the grant ID as the first argument to each command (e.g. nylas email list grant_xyz --json). Use nylas auth list to see your grants.

Why use --yes when sending email?

Without --yes, nylas email send prompts for confirmation interactively. In an agent loop, stdin is not available, so the command would hang. Always use --yes for non-interactive use.

Can I run this in a server or CI environment?

Yes. Authenticate with nylas auth config and set NYLAS_API_KEY in your environment. The CLI reads credentials from config and env vars, so no interactive login is needed after initial setup.

Next steps