Guide

Use Agent Accounts with LangChain

LangChain and LangGraph agents reason; they need tools to act. Giving one email and calendar usually means a provider SDK, OAuth, and a pile of glue. The Nylas CLI shortcut is to wrap each command as a tool: one subprocess call sends mail or reads a calendar, JSON goes in and out, and the agent account handles auth. This guide wraps the CLI as LangChain tools, builds a LangGraph agent around them, and keeps the agent contained by workspace rules the Python can't override.

Written by Pouya Sanooei Software Engineer

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

What does using agent accounts with LangChain mean?

Using an agent account with LangChain means giving a LangChain or LangGraph agent the ability to send and read email and calendar through the CLI, exposed as tools the model can call. The agent reasons about what to do; each tool is a thin Python function that runs one nylas command and returns its result. The agent account provides the identity and the auth, so the Python never touches OAuth or a provider SDK.

This is the subprocess tool pattern: instead of importing an email library, the agent calls a command-line program. It works because the CLI already does the hard parts — authentication, provider differences, JSON formatting — so each tool is a few lines. The model sees a clean function signature; the function shells out.

LangGraph agent calls a Python tool, which shells out to the Nylas CLI and returns JSONLangGraph agentmodel + ReAct loop@tool wrappersubprocess.runNylas CLIagent account, JSONJSON flows back the same way

Why wrap the CLI instead of a provider SDK?

Wrapping the CLI removes the integration layer a provider SDK forces on you. An SDK means OAuth setup, token refresh, and provider-specific clients in your Python process; the CLI means one binary that already handles all of that behind an agent account. Each tool becomes a subprocess call returning JSON, not an SDK object graph you marshal yourself.

The payoff is fewer dependencies and a smaller blast radius. Your LangChain process holds no OAuth tokens and imports no mail library — the credential is the agent account's, managed by the CLI, and a bug in your agent code can't leak a provider token it never had. For the broader pattern of driving the CLI from agent code, see building an AI agent CLI.

How do I wrap a nylas command as a LangChain tool?

A tool is a Python function with the LangChain @tool decorator and a docstring the model reads to decide when to call it. The body runs one CLI command with subprocess.run and returns the output. A shared helper keeps each tool to a few lines:

import json
import subprocess
from langchain_core.tools import tool


def _nylas(*args: str) -> str:
    """Run a nylas CLI command and return stdout (raises on a non-zero exit)."""
    result = subprocess.run(
        ["nylas", *args],
        capture_output=True,
        text=True,
        check=True,
    )
    return result.stdout


@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email from the agent account to a single recipient."""
    _nylas("email", "send", "--to", to, "--subject", subject, "--body", body)
    return f"sent to {to}"

The check=True argument raises on a non-zero exit, so a failed send surfaces as a Python exception the agent can see rather than a silent no-op. Passing arguments as a list — never a shell string — means a recipient or subject from the model can't inject a shell command. That list form is the input-validation boundary for the whole pattern.

How do I give the agent read and calendar tools?

The same helper turns every command into a tool. A read tool parses --json output into Python objects the model can reason over, and a calendar tool books an event — all on the one agent account grant:

@tool
def list_unread() -> list:
    """List unread emails in the agent's inbox with sender and subject."""
    out = _nylas("email", "list", "--unread", "--json")
    return [
        {"id": m["id"], "from": m["from"][0]["email"], "subject": m["subject"]}
        for m in json.loads(out)
    ]


@tool
def list_events(days: int = 1) -> list:
    """List upcoming calendar events for the next N days."""
    out = _nylas("calendar", "events", "list", "--days", str(days), "--json")
    return [{"title": e["title"], "start": e["when"]["start_time"]} for e in json.loads(out)]

Each tool projects only the fields the model needs, which keeps tool output small and the model's context cheap — a list of three-key dicts instead of full message objects. The json.loads() parsing happens in Python, so the model receives clean structured data, not raw text to interpret.

How do I build the agent with LangGraph?

With the tools defined, a LangGraph agent is a few lines. The create_react_agent prebuilt binds the tools to a model and runs the reason-act loop — the model decides which tool to call, the tool runs the CLI command, and the result feeds back into the model. Recent LangChain versions also ship an equivalent create_agent; either binds the same tools:

from langchain_anthropic import ChatAnthropic
from langgraph.prebuilt import create_react_agent

model = ChatAnthropic(model="claude-opus-4-8")
agent = create_react_agent(model, tools=[send_email, list_unread, list_events])

result = agent.invoke({
    "messages": [("user", "Check my inbox and reply to any meeting requests with my availability.")]
})
print(result["messages"][-1].content)

The agent now reads the inbox through list_unread, reasons over the results, and sends through send_email — each step a CLI command behind a tool. Swap the model for any LangChain chat model; the tools don't change because they only depend on the CLI being on the path.

How do I keep the LangChain agent contained?

A LangChain email agent reads untrusted content and can send mail — the lethal trifecta of private data, untrusted input, and external communication. You can't fully prevent a prompt injection in an email from steering the model, so the containment lives outside the agent's decision loop, on the agent account's workspace. An outbound rule blocks unexpected recipients before the send command reaches the pipeline:

nylas agent rule create \
  --name "Block flagged langchain-agent outbound" \
  --trigger outbound \
  --condition recipient.domain,is,exfil.example \
  --action block

Because the rule is enforced by the platform, not the Python, the model can't prompt its way past it — even a tool call the model was tricked into making hits the workspace rule. Pair it with a policy send cap so a runaway loop can't flood recipients. The full containment pattern is in Stop Your AI Agent From Going Rogue, and the rule reference is in Agent Rules and Policies.

LangChain tools vs MCP

Wrapping the CLI as LangChain tools and connecting over MCP solve the same problem two ways. Subprocess tools are simplest when the agent is a single Python program — no server, no protocol, just functions. MCP is the better fit when many different agents or clients need the same email tools through a standard interface.

ConcernLangChain subprocess toolsMCP server
SetupA function per command, in-processA server the client connects to
Best forOne Python agentMany agents/clients, one interface
CouplingTools live in your codeTools live behind a protocol

Both reach the same agent account and the same workspace guardrails. For the MCP route, see setting up an email MCP server and the MCP vs API comparison.

Next steps