| requires.env | RESEND_API_KEY,DEFAULT_FROM_EMAIL,DEFAULT_FROM_NAME |
| license | Apache-2.0 |
| name | email-resend |
| description | Send and receive emails using the Resend API. Use for: (1) sending emails directly via Resend API, (2) receiving email notifications via cron, (3) drafting replies with proper threading, (4) downloading attachments from inbound emails.
**Required env vars:** RESEND_API_KEY (API key), DEFAULT_FROM_EMAIL (sender email), DEFAULT_FROM_NAME (sender name). **License: Apache-2.0** — See LICENSE file for details.
|
License
Apache License 2.0 — See LICENSE file for full text.
Email via Resend
Send and receive emails using the Resend API.
Configuration
No config file needed. The skill auto-discovers settings from:
- OpenClaw context — channel, chat_id, user email/name (from context)
- Environment variables — for API keys
- Interactive prompts — asks user if required info missing
Required Environment Variables
export RESEND_API_KEY="re_123456789"
export DEFAULT_FROM_EMAIL="you@example.com"
export DEFAULT_FROM_NAME="Your Name"
First-Time Setup
When the skill is first invoked, the sub-agent should:
-
Check context — OpenClaw context already has:
context.user.email (from USER.md)
context.channel (from current session)
context.chat_id
context.thread_id (for topics)
-
Check memory — Use memory_search tool:
- Query: "email notification channel preference"
- Query: "email from address"
-
If missing, ask user — Via chat message:
- "Which email should I send from?"
- "Which channel for email notifications?"
-
Commit to memory — Write preferences to persist across sessions:
write path="memory/email-preferences.md" content="---
from_email: $EMAIL
from_name: $NAME
notification_channel: $CHANNEL
chat_id: \"$CHAT_ID\"
thread_id: \"$THREAD_ID\"
---
# Email Notification Preferences
Saved $(date)
"
This ensures memory_search finds it in future sessions.
Format (YAML frontmatter):
---
from_email: you@company.com
from_name: Your Name
notification_channel: telegram # or discord, signal, whatsapp, etc - from memory or ask user
chat_id: "123456789"
thread_id: "334" # optional, for topics
---
Context Fields (Available in Sub-Agent)
| Field | Source | Example |
|---|
user.email | USER.md | you@company.com |
user.name | USER.md | Your Name |
channel | OpenClaw | from context |
chat_id | OpenClaw | 123456789 |
thread_id | OpenClaw | 334 |
The skill uses these directly from OpenClaw context — no parsing needed.
Usage
Inbound (Receive)
Cron Setup
openclaw cron add \
--name "email-resend-inbound" \
--every 15m \
--session-target isolated \
--message "Follow instructions in skills/email-resend/cron-prompts/email-inbound.md" \
--to "YOUR_CHAT_ID" \
--announce
Manual Check
python3 skills/email-resend/scripts/inbound.py
Notification Format
Each new email triggers a notification with:
- From, Subject, Date
- Body preview (~2000 chars)
- Attachment list (if any)
- Importance: 🔥 HIGH / 📅 MEETING / 📬 NORMAL
Acknowledge Flow
Reply to the notification message (using notification reply) to acknowledge, or type:
done / ack - mark as read
Use draft-reply.py to compose replies with proper quoting.
Important: Always use inline replies ([[reply_to_current]]) to keep messages linked in the thread. This enables:
- Proper custody chain tracking
- Reply-to-email tracing
- Better conversation flow
CRITICAL: When responding via OpenClaw message tool, use replyTo parameter (not [[reply_to_current]] tag):
message(action="send", channel="<from-context>", replyTo="<msg_id>", ...)
Scripts
| Script | Purpose |
|---|
inbound.py | Check emails, send notifications |
draft-reply.py | Draft reply workflow with quoting & threading |
outbound.py | Send emails directly |
download_attachment.py | Download attachments from inbound emails |
Downloading Attachments
To download attachments from an inbound email:
python3 scripts/download_attachment.py <email_id> --list
python3 scripts/download_attachment.py <email_id> --output-dir ./attachments
python3 scripts/download_attachment.py <email_id> --attachment-id <attachment_id>
Note: The API path is /emails/receiving/{email_id}/attachments (not the standard /emails/ path).
State Files
memory/email-resend-inbound-notified.json — pending/acknowledged emails
memory/email-message-map.json — notification message_id → email_id (legacy)
memory/email-custody-chain.json — Full DAG of email → notification → actions
memory/email-msg-to-chain.json — notification message_id → chain lookup
memory/email-draft-state.json — Active draft state (email_id, status, reply_content)
See docs/custody-chain.md for DAG design.
Outbound (Send)
python3 skills/email-resend/scripts/outbound.py \
--to "recipient@example.com" \
--subject "Hello" \
--body "Message text"
python3 skills/email-resend/scripts/outbound.py \
--to "recipient@example.com" \
--subject "Here's the file" \
--body "See attachment" \
--attachment ./file.pdf \
--attachment ./image.png
⚠️ CRITICAL: Email Threading Rule (2026-02-22)
MANDATORY: Always use draft-reply.py for replying to emails.
This is non-negotiable. Failure to follow this rule will result in broken Gmail threading.
Why This Matters
- Gmail threads emails based on
In-Reply-To AND References headers
- Using wrong headers = reply appears as NEW thread = context lost
- There's no way to fix this after sending
✅ Correct Workflow (ALWAYS USE THIS)
python3 skills/email-resend/scripts/draft-reply.py start <email_id>
python3 skills/email-resend/scripts/draft-reply.py content "Your reply"
python3 skills/email-resend/scripts/draft-reply.py send
⚠️ CRITICAL: Approval Execution Rule (2026-02-22)
When user approves a draft, you MUST execute the send command immediately.
The mistake to avoid:
- ❌ Show draft for approval → User says "send" → Only acknowledge, don't execute
- ✅ Show draft for approval → User says "send" → RUN
draft-reply.py send → Then confirm
Correct workflow:
1. Show draft for approval
2. User replies "approve", "send", "yes", or "ok"
3. IMMEDIATELY run: draft-reply.py send
4. Only THEN confirm to user
Never:
- Only acknowledge the approval without executing
- Ask for confirmation after user already approved
- Wait to send - do it immediately
❌ NEVER Do These Things
NEVER use outbound.py for replies:
python3 skills/email-resend/scripts/outbound.py \
--to "x@y.com" --subject "Re: Original" --body "Reply"
NEVER manually construct --reply-to flags:
python3 skills/email-resend/scripts/outbound.py \
--to "x@y.com" --subject "Re: Original" --body "Reply" \
--reply-to "<some-guess>@resend"
NEVER skip the workflow when subject starts with "Re:":
python3 skills/email-resend/scripts/outbound.py \
--to "x@y.com" --subject "Re: Previous Thread" --body "Quick reply"
outbound.py Only For New Emails
outbound.py is for new emails only (not replies):
- First contact
- Announcements
- Emails where you intentionally want a NEW thread
For anything that could be a reply, use draft-reply.py.
Requirements
RESEND_API_KEY environment variable set
- Python
requests library
Draft Reply Best Practices
When composing a reply via draft-reply.py:
-
Always quote the original — Include the original message with > prefix so recipient knows what you're responding to
-
Use proper threading — Set In-Reply-To and References headers using the original email's Message-ID
-
Keep subject line — Start with Re: prefix to maintain thread (but avoid "Re: Re:")
-
Structure:
Your reply here
---
On [date] [original sender] wrote:
> quoted original message
> continues here
-
Multiple replies supported — After sending, draft is marked as "sent" so you can reply again to the same thread. Use resume command to continue.
-
No double Re: — If original subject already starts with "Re:", don't add another
-
Custody Chain — Track full lineage:
- Email → notification → All replies/actions
- DAG structure with parent links
- Any message traces back to original email
Draft Reply Commands
| Command | Purpose |
|---|
start <email_id> | Start a draft reply to an email |
resume | Continue a sent thread to reply again |
content "text" | Set reply content |
send | Send the reply |
cancel | Cancel the draft |
status | Show current draft status |
After sending, use resume to reply again to the same thread — threading headers are preserved.
Run tests:
python3 skills/email-resend/tests/test_inbound.py
Expected: 40 tests, all passing.
If tests fail:
- Check which test failed and why
- Fix the feature/code to match expected behavior
- Or update tests if feature intentionally changed