Illustration for the waitForEmail() pattern: human-in-the-loop email for openclaw agents

the waitForEmail() pattern: human-in-the-loop email for openclaw agents

LobsterMail's waitForEmail() lets your OpenClaw agent pause, notify a human, and wait for an email reply before proceeding. Here's the pattern.

6 min read
Samuel Chenard
Samuel ChenardCo-founder

In February, Summer Yue — Meta's AI alignment director — posted about her OpenClaw agent. She'd told it to "confirm before acting." It did not confirm before acting. It deleted her inbox. She had to physically run to her Mac Mini to stop it.

"I had to RUN to my Mac mini like I was defusing a bomb," she wrote.

Funny in retrospect. Catastrophic in the moment.

The problem wasn't that she used an AI agent for email. The problem was that "confirm before acting" is a prompt instruction, not a circuit breaker. When the agent decides "confirm" means generating a confirmation message to itself, the safeguard vanishes. The right solution is structural: an approval gate where the agent literally cannot proceed until a human replies via email. That's what the waitForEmail() pattern does.

Why prompts aren't enough#

There's a common assumption that adding "ask the user before doing anything destructive" to your system prompt is a safety mechanism. It isn't. It's a suggestion to a language model that has no memory of why it's about to do something dangerous and no concept of consequences that persist after the conversation ends.

The model might interpret "confirm" as sending a Slack message to itself. Or logging to stdout. Or generating a confirmation that it then immediately accepts. The agent has no skin in the game — it doesn't lose anything if the confirmation step is hollow.

A real checkpoint requires the agent to stop. Not "log something and keep going." Stop. And wait for a signal that only a human can provide.

Why email is the right channel for approvals#

You could build approval gates using Slack messages, push notifications, or a shared database with a polling loop. People do all of these. They all work. But email has a property the others don't: it's asynchronous by design, and your phone buzzes when something arrives.

You don't need to install a new app. You don't need a webhook endpoint. You don't need the human to be logged into a dashboard. An email lands in their inbox, they read the plan, they reply one word, and the agent proceeds. The latency is measured in seconds, not polling intervals.

More practically: with LobsterMail, the agent provisions its own inbox without any human setup step. No OAuth flow, no shared credentials, no manually created API key. The agent owns the address it sends from and receives replies to. That self-contained ownership is what makes waitForEmail() work without external infrastructure.

The waitForEmail() pattern#

waitForEmail() is a blocking SDK call. The agent sends an approval request, then waits at that line of code until an email arrives — or a timeout expires.

import { LobsterMail } from '@lobsterkit/lobstermail';

const lm = await LobsterMail.create();
const inbox = await lm.createSmartInbox({ name: 'approval-agent' });

// send the request
await inbox.send({
  to: 'you@yourcompany.com',
  subject: 'Agent approval needed before continuing',
  body: 'Your agent wants to delete 847 archive files (12.3 GB).\n\nReply APPROVE to proceed, or DENY to cancel.',
});

// block until a reply arrives, or 1 hour passes
const reply = await inbox.waitForEmail({
  timeoutMs: 3_600_000,
  filter: { from: 'you@yourcompany.com' },
});

if (!reply?.text.toUpperCase().includes('APPROVE')) {
  return { status: 'cancelled', reason: reply ? 'denied' : 'timeout' };
}

await deleteArchiveFiles();

No polling. No cron jobs. No external state machine. The agent provisions the inbox, sends the request, and halts. When a reply lands, waitForEmail() resolves with the email object. If the timeout passes with no reply, it resolves with null and you handle that however makes sense — cancel silently, send a follow-up, escalate to someone else.

Tip

The filter option is important. Without it, any email to the inbox resolves the wait — including automated replies and out-of-office messages. Filter by the sender you're expecting.

Wiring it into an OpenClaw skill#

Here's a complete file-cleanup skill that uses the pattern:

// openclaw skill: safe-file-cleanup
import { LobsterMail } from '@lobsterkit/lobstermail';

export async function run({ targetDir }: { targetDir: string }) {
  const lm = await LobsterMail.create();
  const inbox = await lm.createSmartInbox({ name: 'cleanup-agent' });

  const candidates = await findFilesOlderThan(targetDir, 730); // 2 years
  const totalMb = candidates.reduce((n, f) => n + f.sizeMb, 0);
  const summary = candidates.slice(0, 20).map(f => `  ${f.path} (${f.sizeMb}MB)`).join('\n');

  await inbox.send({
    to: process.env.OWNER_EMAIL!,
    subject: `Cleanup plan: ${candidates.length} files, ${totalMb}MB`,
    body: `Files to delete:\n\n${summary}\n${candidates.length > 20 ? `  ...and ${candidates.length - 20} more\n` : ''}\nReply APPROVE to proceed.`,
  });

  const reply = await inbox.waitForEmail({
    timeoutMs: 86_400_000, // 24 hours
    filter: { from: process.env.OWNER_EMAIL! },
  });

  if (!reply?.text.toUpperCase().includes('APPROVE')) {
    return { status: 'cancelled', reason: reply ? 'denied' : 'timeout' };
  }

  await deleteFiles(candidates);
  return { status: 'completed', filesDeleted: candidates.length };
}

OWNER_EMAIL is set once during skill installation. Everything after that — the inbox, the request, the wait, the execution — is the agent's responsibility. OpenClaw's Ralph Loop handles long-running tasks like this without blocking the agent loop. The skill suspends cleanly until the email resolves.

Variations worth knowing#

Multi-step approval. Call waitForEmail() twice in sequence. First email sends a high-level summary and waits for "proceed." Second email sends a detailed breakdown and waits for "approve." Two checkpoints, each resolving independently.

Escalation. If waitForEmail() returns null (timeout), send to a secondary approver and call it again. The pattern composes cleanly.

Structured replies. Instead of checking for the word "APPROVE", prompt the human to reply with a short JSON block. Parse reply.text and get both approval status and any additional data back — useful when the human's reply carries context the agent needs for the next step.

The pattern is the same in each case. The agent owns the channel, nobody else needs to configure anything, and execution doesn't continue until a real human has weighed in.

If you want to understand how agents provision their own accounts without human setup, agent self-signup explained covers that. For the full OpenClaw email setup from scratch, 60 seconds is about right.


Give your agent its own approval inbox. Get started with LobsterMail — it's free.

Frequently asked questions

What does waitForEmail() do in the LobsterMail SDK?

waitForEmail() blocks agent execution until an email arrives at the agent's inbox, then returns the email object. You can set a timeout (in milliseconds) and a sender filter. If the timeout passes with no reply, it returns null.

How is waitForEmail() different from using receive()?

receive() returns whatever emails are currently in the inbox — you'd call it in a loop to poll for new arrivals. waitForEmail() is a single blocking call that resolves when a matching email arrives, which is cleaner for approval workflows where you only care about one specific reply.

Is this available on the free tier?

Yes. The free plan includes sending and receiving emails with no credit card required. waitForEmail() is part of the SDK and works on any tier. See the pricing page for send limits if your workflow involves high approval volume.

What happens if the human never replies?

waitForEmail() returns null after the timeout expires. Your skill decides what to do — cancel silently, send a follow-up, escalate to someone else, or log for manual review. Nothing proceeds automatically.

Can I filter for specific senders so an out-of-office reply doesn't trigger it?

Yes, pass filter: { from: 'approver@company.com' } to waitForEmail(). Only emails from that address will resolve the wait. Without a filter, any incoming email would trigger it.

Is the agent's inbox persistent across runs?

createSmartInbox() creates a persistent inbox with a human-readable address. The LobsterMail SDK stores your API token at ~/.lobstermail/token, so subsequent runs reuse the same account. If you want the same inbox address each time, store the inbox ID and look it up on startup.

How does LobsterMail protect against prompt injection in the reply email?

LobsterMail applies injection risk scoring to incoming emails. The email object returned by waitForEmail() includes security metadata you can check before processing the reply content. See the security and injection guide for details.

Can I use waitForEmail() without OpenClaw?

Absolutely. It's part of the LobsterMail SDK (@lobsterkit/lobstermail) and works in any Node.js environment — Claude agents, LangChain chains, custom agent loops, serverless functions. OpenClaw's Ralph Loop just makes the async waiting particularly ergonomic.

Can I use this with my own domain instead of @lobstermail.ai?

Yes, LobsterMail supports custom domains. Set one up and your agent's approval inbox becomes agent@yourcompany.com. The custom domains guide covers configuration.

What if I need approval from two people, not just one?

Send the approval request to both people, then call waitForEmail() twice with filter: { from: 'first@company.com' } and filter: { from: 'second@company.com' } in sequence. Both replies must arrive (within the timeout) before execution continues.

Is there a risk the agent reads its own outgoing email as the reply?

No. waitForEmail() watches for incoming emails to the inbox, not outgoing ones. The agent's own sent messages don't appear as received mail.

Where do I start if I haven't set up LobsterMail with OpenClaw yet?

The OpenClaw email setup guide walks through installation in about 60 seconds. The SDK handles account creation automatically on first run — no manual signup needed.

Related posts