One of the most common points where LangChain agents break down in production is email verification. The agent can navigate a signup form, fill in details, click submit — and then it hits a wall. It cannot receive the OTP or click the magic link that the service sends back. This guide shows you how to close that gap by wrapping AgentMailr as a native LangChain tool.

What We Are Building

A set of three LangChain tools that give any agent full email capabilities: creating a provisioned inbox, waiting for and extracting OTPs, and fetching the raw body of any incoming email. Once wrapped as tools, your agent can use them like any other action in a chain or agent executor.

Installation

npm install agentmailr langchain @langchain/openai

Wrapping AgentMailr as LangChain Tools

LangChain's DynamicTool lets you wrap any async function as a tool. Each tool needs a name, description (used by the LLM to decide when to call it), and a function.

import { DynamicTool } from "@langchain/core/tools";
import AgentMailr from "agentmailr";

const client = new AgentMailr({ apiKey: process.env.AGENTMAILR_API_KEY });

// Store inbox state across tool calls in the same run
let activeInboxId: string | null = null;

export const createInboxTool = new DynamicTool({
  name: "create_email_inbox",
  description:
    "Creates a temporary email inbox for this agent run. " +
    "Returns the email address to use on signup forms. " +
    "Call this before navigating to any signup page.",
  func: async () => {
    const inbox = await client.inboxes.create();
    activeInboxId = inbox.id;
    return `Created inbox: ${inbox.address}`;
  },
});

export const waitForOTPTool = new DynamicTool({
  name: "wait_for_otp",
  description:
    "Waits for an OTP or verification code to arrive in the active inbox. " +
    "Call this immediately after submitting a form that triggers an email. " +
    "Returns the extracted numeric code as a string.",
  func: async () => {
    if (!activeInboxId) return "Error: no inbox created yet. Call create_email_inbox first.";
    const { otp } = await client.messages.waitForOTP({
      inboxId: activeInboxId,
      timeout: 45_000,
    });
    return otp;
  },
});

export const getLatestEmailTool = new DynamicTool({
  name: "get_latest_email",
  description:
    "Fetches the full body of the most recent email in the active inbox. " +
    "Use this to extract magic links or read confirmation email content.",
  func: async () => {
    if (!activeInboxId) return "Error: no inbox created yet.";
    const messages = await client.messages.list({ inboxId: activeInboxId });
    if (!messages.length) return "No emails received yet.";
    const latest = messages[0];
    return `From: ${latest.from}
Subject: ${latest.subject}

${latest.body}`;
  },
});

Connecting the Tools to an Agent

Now wire these tools into a LangChain agent. Here is a complete example using the OpenAI functions agent, which is well-suited for structured tool use:

import { ChatOpenAI } from "@langchain/openai";
import { createOpenAIFunctionsAgent, AgentExecutor } from "langchain/agents";
import { pull } from "langchain/hub";
import { createInboxTool, waitForOTPTool, getLatestEmailTool } from "./tools/email";

const tools = [createInboxTool, waitForOTPTool, getLatestEmailTool];

const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });
const prompt = await pull("hwchase17/openai-functions-agent");

const agent = await createOpenAIFunctionsAgent({ llm, tools, prompt });
const executor = new AgentExecutor({ agent, tools, verbose: true });

const result = await executor.invoke({
  input:
    "Sign up for a free account at https://example.com/signup. " +
    "Use the email inbox tool to get an email address. " +
    "After submitting the form, wait for the OTP and enter it to complete verification.",
});

console.log(result.output);

Using with LangGraph

If you are using LangGraph for stateful multi-step agents, the same tools work without modification. You can store the inbox ID in the graph state so it persists across nodes:

import { StateGraph, Annotation } from "@langchain/langgraph";
import AgentMailr from "agentmailr";

const client = new AgentMailr({ apiKey: process.env.AGENTMAILR_API_KEY });

const StateAnnotation = Annotation.Root({
  inboxId: Annotation<string>(),
  inboxAddress: Annotation<string>(),
  otp: Annotation<string>(),
});

const provisionInbox = async (state: typeof StateAnnotation.State) => {
  const inbox = await client.inboxes.create();
  return { inboxId: inbox.id, inboxAddress: inbox.address };
};

const collectOTP = async (state: typeof StateAnnotation.State) => {
  const { otp } = await client.messages.waitForOTP({
    inboxId: state.inboxId,
    timeout: 30_000,
  });
  return { otp };
};

const graph = new StateGraph(StateAnnotation)
  .addNode("provision_inbox", provisionInbox)
  .addNode("collect_otp", collectOTP)
  .addEdge("__start__", "provision_inbox")
  .addEdge("provision_inbox", "collect_otp")
  .compile();

Tips for Production Use

  • One inbox per agent run: Never share an inbox between concurrent runs. AgentMailr inboxes are cheap to create — provision one per workflow execution.
  • Set your timeout higher than you think you need: Some services take 15-20 seconds to deliver OTP emails. A 45-second timeout is a safe default for most services.
  • Delete inboxes after use: Call client.inboxes.delete(inboxId) at the end of your workflow. This keeps your account clean and prevents stale inboxes from accumulating.
  • Use custom domains for higher trust: Some services reject OTPs to disposable email domains. AgentMailr supports custom domains so your agents receive mail at your own domain.