Inbox Zero is easy to tweet about and painful to achieve. One OpenClaw user wiped out 10,437 unread emails on day one by letting the agent do the boring parts: categorizing, labeling, archiving, and even drafting replies. This post shows exactly how to set up the same workflow with Gmail. No previous OpenClaw experience required.
Why OpenClaw beats filters + keyboard shortcuts
Gmail filters help, but they don’t learn. OpenClaw does. The agent can:
- Pull every new message via the Gmail skill (using Google’s REST v1 API under the hood).
- Read message bodies with the same context window used for Slack, Discord, and 800+ other Composio skills.
- Store long-term preferences in
memory.jsonso it keeps improving without re-training. - Run on a schedule (cron-like) or react in real-time via Gmail push notifications.
The trade-off: setup takes 20–30 minutes, OAuth scopes must be approved, and you need to be okay with an AI reading your mail. If that’s a non-starter, stop here.
Prerequisites: Node 22+, a Google Cloud project, and a spare 30 min
You’ll need:
- Node.js 22.4 or newer (OpenClaw v3.4.2 dropped support for Node 20 last month).
- A Google Cloud project with Gmail API enabled.
- OAuth credentials (desktop type if you’re running locally, web type if you’re on ClawCloud).
- OpenClaw >= 3.4.2 installed globally or an active ClawCloud workspace.
Local install commands:
npm install -g openclaw@3.4.2
openclaw login # opens browser for GitHub auth
openclaw new inbox-agent
cd inbox-agent
Skip the above if you’re using ClawCloud; the dashboard creates the project scaffolding for you.
Connecting Gmail to OpenClaw
1. Add the Gmail skill
openclaw skill:add gmail
The CLI asks for:
- Client ID
- Client secret
- Redirect URI (use
http://localhost:34589/callbacklocally or the ClawCloud URI)
Scopes requested by the skill (as of v0.9.1):
https://www.googleapis.com/auth/gmail.readonly
https://www.googleapis.com/auth/gmail.modify
https://www.googleapis.com/auth/gmail.send
Approve the consent screen; you’ll see a token saved under .claw/credentials.json.
2. Verify basic connectivity
openclaw shell
> gmail.listThreads({labelIds:["INBOX"], maxResults:3})
If you get three thread IDs back, the pipe is open. If not, double-check OAuth scopes and make sure the Gmail API is enabled in the Google Cloud dashboard. Yes, I still forget this step.
Teaching OpenClaw your email priorities
OpenClaw needs context. Start by populating memory with examples:
openclaw memory:add "Priority senders" "Emails from my boss, the SRE on-call list, and Stripe notifications are always urgent."
openclaw memory:add "Mute" "Automated build success messages can be archived without reading."
openclaw memory:add "Newsletters" "Anything from *.substack.com should be saved to the 'Reading' label and archived from the inbox."
Under the hood these land in memory.json and get embedded with OpenAI 4o (the default model in config.yml). Retrieval is purely semantic; exact addresses are optional but help.
The agent will reference memory every time it decides what to do with an incoming message. Think of it as supervised fine-tuning without the GPU bill.
Auto-categorization with a simple prompt + filters
Create skills/gmail/auto-triage.js:
module.exports = async ({ gmail, message, memory }) => {
const { subject, from } = message;
// Hard rules first
if (from.endsWith("@substack.com")) {
return { label: "Reading", archive: true };
}
if (subject.match(/build succeeded/i)) {
return { archive: true };
}
// Delegate fuzzy logic to the LLM
const prompt = `You are my email assistant. Context: ${JSON.stringify(memory)}.
Decide category: urgent, later, or archive. Respond with JSON.`;
const { category } = await openclaw.llm(prompt + "\nEmail:" + message.snippet);
switch (category) {
case "urgent":
return { label: "!urgent", markRead: false };
case "later":
return { label: "Later", archive: false };
default:
return { archive: true };
}
};
Wire it up in config.yml:
triggers:
- skill: gmail
on: message.received
handler: skills/gmail/auto-triage.js
Commit and push; ClawCloud hot-reloads in ~3 seconds. Local users run openclaw daemon &.
Batch actions: archive, label, respond
Now the fun part: nuking thousands of existing emails. We’ll operate in chunks of 500 to stay under Gmail’s quota limits.
1. Dry-run query
openclaw shell
> const threads = await gmail.search("in:inbox older_than:2y -is:starred", 0, 5000)
> threads.length
5000
If the result count matches expectation, proceed.
2. Bulk archive and label
await Promise.all(
threads.map(id => gmail.modify(id, {
removeLabelIds: ["INBOX"],
addLabelIds: ["Archive_2019_2022"]
}))
);
CPU-bound on the Gmail side. Expect ~40 QPS with the default user quota. The original user who cleared 10k messages hit the daily limit after ~9k; they resumed next morning.
3. Autorespond for predictable senders
threads
.filter(id => /* logic to match invoices */)
.forEach(async id => {
const msg = await gmail.get(id);
const reply = await openclaw.llm(`Draft a short confirmation that I've received the invoice:\n${msg.snippet}`);
await gmail.send({
to: msg.from,
subject: "Re: " + msg.subject,
body: reply
});
});
Yes, AI-generated email is risky. We gate with manual review in staged mode:
openclaw env:set REVIEW_MODE=true
If REVIEW_MODE is true, drafts are saved but not sent. You can scan them in Drafts and hit send manually. I keep it on.
Setting up ongoing maintenance rules
1. Cron schedule
Add to config.yml:
schedules:
- name: inbox-sweep
cron: "*/15 * * * *" # every 15 minutes
handler: skills/gmail/auto-triage.js
OpenClaw’s scheduler is powered by node-cron@3.1.3; jobs run inside the same worker process, so keep them short (<1 min) or spawn child processes.
2. Hard stop on weekends
if (new Date().getDay() === 0 || new Date().getDay() === 6) {
console.log("Weekend detected – skipping auto triage");
return;
}
No point reminding yourself about work email on Sunday.
3. Daily stats ping
I like a Slack summary at 18:00:
module.exports = async ({ gmail, slack }) => {
const count = await gmail.count("is:unread in:inbox");
await slack.post("#ops", `📬 Unread after sweep: ${count}`);
};
Hook the above to cron: "0 18 * * *". Replace Slack with Telegram/Discord — same abstraction, just swap the skill.
Safety nets, rate limits, and rollback options
Gmail doesn’t support transactions. If your script mislabels 2,000 emails, you’ll be hand-picking them from All Mail. Tips:
- Test with
maxResults:50. - Enable
--dry-runflag (OpenClaw CLI v3.5 adds this globally). - Use distinct labels (e.g.,
OC_ARCHIVE_YYYYMMDD) so undo queries are trivial. - Keep an export of
threads.map(id => ({ id, labels }))inbackup.json.
Rate limits: 250 queries per user per second, 500 writes per day. OpenClaw batches writes with exponential backoff; you can tune batch size in ~/.claw/config.
From 10,437 unread to a quiet inbox
If you followed along you now have:
- Gmail connected via OAuth with read/write/send scopes.
- An auto-triage handler that mixes hard rules and LLM judgement.
- Batch scripts to archive, label, and reply to historical mail.
- A cron schedule keeping things clean every 15 minutes.
- Slack (or equivalent) daily stats for psychological safety.
The next step is to tweak memory and filters until the false-positive rate is under 5%. After that, you’ll forget about email until someone asks how you hit Inbox Zero on a Monday morning.
Happy pruning.