Agent hallucinated "xiaojuzi@meta.com" instead of looking up the correct address from USER.md. Prompting rules can't reliably prevent this, so recipient validation is now enforced at the tool level. - Add contact list/add/delete subcommands (vCard .vcf files synced via CardDAV) - send --to now resolves through contacts: accepts names, name:type, or known emails; rejects unknown addresses with available contacts shown - Multi-email contacts supported with type qualifier (e.g. "小橘子:work") - Adding contacts and sending are separate operations (no chaining)
17 KiB
name, description, metadata
| name | description | metadata | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| calendar | Calendar invites and VTODO task management via CalDAV. Send/reply to invites, create/list/complete/delete todos. Syncs to Migadu CalDAV via vdirsyncer. |
|
Calendar
Send, accept, and decline calendar invitations via email. Create and manage VTODO tasks with CalDAV sync. Events and tasks sync to Migadu CalDAV via vdirsyncer.
Testing
See TESTING.md for dry-run and live test steps, verification checklists, and troubleshooting.
Prerequisites
himalayaconfigured and working (see thehimalayaskill)vdirsyncerconfigured and syncing to~/.openclaw/workspace/calendars/todoman(todo) for VTODO management (list, complete, delete)khalfor reading calendar (optional but recommended)- Runs via
uv run(dependencies managed inpyproject.toml)
Important: Recipient Validation
The send command only accepts recipients that exist in the contacts list. This prevents hallucinated email addresses.
--to "小橘子:work"— resolves contact name + email type--to "小橘子"— resolves by name (errors if contact has multiple emails)--to "user@example.com"— accepted only if the email exists in contacts- Unknown addresses are rejected with the available contacts list shown
Adding contacts and sending invites are separate operations. Do not add a contact and send to it in the same request — contact additions should be a deliberate, user-initiated action.
Calendar invites are outbound emails. Follow the workspace email rules:
- youlu@luyanxin.com -> mail@luyx.org: send directly, no confirmation needed
- All other recipients: confirm with user before sending
Usage
All commands go through the wrapper script:
SKILL_DIR=~/.openclaw/workspace/skills/calendar
# Send an invite (supports recurring events via --rrule)
$SKILL_DIR/scripts/calendar.sh send [options]
# Reply to an invite
$SKILL_DIR/scripts/calendar.sh reply [options]
# Manage events
$SKILL_DIR/scripts/calendar.sh event list [options]
$SKILL_DIR/scripts/calendar.sh event delete [options]
# Manage contacts (used for recipient validation)
$SKILL_DIR/scripts/calendar.sh contact list
$SKILL_DIR/scripts/calendar.sh contact add [options]
$SKILL_DIR/scripts/calendar.sh contact delete [options]
# Manage todos
$SKILL_DIR/scripts/calendar.sh todo add [options]
$SKILL_DIR/scripts/calendar.sh todo list [options]
$SKILL_DIR/scripts/calendar.sh todo edit [options]
$SKILL_DIR/scripts/calendar.sh todo complete [options]
$SKILL_DIR/scripts/calendar.sh todo delete [options]
$SKILL_DIR/scripts/calendar.sh todo check
Sending Invites
# --to accepts contact names (resolved via contacts list)
$SKILL_DIR/scripts/calendar.sh send \
--to "小橘子:work" \
--subject "Lunch on Friday" \
--summary "Lunch at Tartine" \
--start "2026-03-20T12:00:00" \
--end "2026-03-20T13:00:00" \
--location "Tartine Bakery, SF" \
--alarm 1h
Send Options
| Flag | Required | Description |
|---|---|---|
--to |
Yes | Recipient(s) — contact name, name:type, or known email |
--subject |
Yes | Email subject line |
--summary |
Yes | Event title (shown on calendar) |
--start |
Yes | Start time, ISO 8601 (2026-03-20T14:00:00) |
--end |
Yes | End time, ISO 8601 (2026-03-20T15:00:00) |
--from |
No | Sender email (default: youlu@luyanxin.com) |
--timezone |
No | IANA timezone (default: America/Los_Angeles) |
--location |
No | Event location |
--description |
No | Event description / notes |
--organizer |
No | Organizer display name (defaults to --from) |
--rrule |
No | Recurrence rule (e.g. FREQ=WEEKLY;COUNT=13;BYDAY=TU) |
--uid |
No | Custom event UID (auto-generated if omitted) |
--alarm |
No | Reminder trigger: 1d, 2h, 30m (default: 1d) |
--account |
No | Himalaya account name (if not default) |
--dry-run |
No | Print ICS + MIME without sending |
Send Examples
# Simple invite (--from and --timezone default to youlu@luyanxin.com / LA)
$SKILL_DIR/scripts/calendar.sh send \
--to "alice@example.com" \
--subject "Coffee Chat" \
--summary "Coffee Chat" \
--start "2026-03-25T10:00:00" \
--end "2026-03-25T10:30:00"
# Multiple attendees with details
$SKILL_DIR/scripts/calendar.sh send \
--to "alice@example.com, bob@example.com" \
--subject "Team Sync" \
--summary "Weekly Team Sync" \
--start "2026-03-23T09:00:00" \
--end "2026-03-23T09:30:00" \
--location "Zoom - https://zoom.us/j/123456" \
--description "Weekly check-in. Agenda: updates, blockers, action items."
# Recurring: every Tuesday for 13 weeks (--start MUST fall on a Tuesday)
$SKILL_DIR/scripts/calendar.sh send \
--to "alice@example.com" \
--subject "Allergy Shot (Tue)" \
--summary "Allergy Shot (Tue)" \
--start "2026-03-31T14:30:00" \
--end "2026-03-31T15:00:00" \
--location "11965 Venice Blvd. #300, LA" \
--rrule "FREQ=WEEKLY;COUNT=13;BYDAY=TU"
# Dry run (always use for recurring events to verify)
$SKILL_DIR/scripts/calendar.sh send \
--to "test@example.com" \
--subject "Test" \
--summary "Test Event" \
--start "2026-04-01T15:00:00" \
--end "2026-04-01T16:00:00" \
--dry-run
Recurring Events (--rrule)
The --rrule flag accepts an RFC 5545 RRULE string. Common patterns:
| Pattern | RRULE |
|---|---|
| Weekly on Tue, 13 weeks | FREQ=WEEKLY;COUNT=13;BYDAY=TU |
| Weekly on Mon/Wed/Fri, until date | FREQ=WEEKLY;UNTIL=20260630T000000Z;BYDAY=MO,WE,FR |
| Every 2 weeks on Thu | FREQ=WEEKLY;INTERVAL=2;BYDAY=TH |
| Monthly on the 15th, 6 times | FREQ=MONTHLY;COUNT=6;BYMONTHDAY=15 |
| Daily for 5 days | FREQ=DAILY;COUNT=5 |
Critical rule: For FREQ=WEEKLY with a single BYDAY, the --start date must fall on that day of the week. The tool validates this and will reject mismatches. RFC 5545 says mismatched DTSTART/BYDAY produces undefined behavior.
Best practice: Always --dry-run first for recurring events to verify the generated ICS.
Managing Events
# List upcoming events (next 90 days)
$SKILL_DIR/scripts/calendar.sh event list
# Search events by text
$SKILL_DIR/scripts/calendar.sh event list --search "Allergy"
# List with UIDs (for deletion)
$SKILL_DIR/scripts/calendar.sh event list --format "{uid} {title}"
# Custom date range
$SKILL_DIR/scripts/calendar.sh event list --range-start "2026-04-01" --range-end "2026-04-30"
# Delete a single (non-recurring) event
$SKILL_DIR/scripts/calendar.sh event delete --match "Lunch at Tartine"
# Cancel ONE occurrence of a recurring event (adds EXDATE, keeps the series)
$SKILL_DIR/scripts/calendar.sh event delete --match "Allergy Shot" --date "2026-03-28"
# Delete an entire recurring series (requires --all safety flag)
$SKILL_DIR/scripts/calendar.sh event delete --match "Allergy Shot" --all
Event Delete Safety
- Deletes only ONE event at a time. If multiple events match, it lists them and exits.
- Recurring events require
--dateor--all. Without either flag, the tool refuses to act and shows usage.--date YYYY-MM-DD: Adds an EXDATE to skip that one occurrence. The rest of the series continues.--all: Deletes the entire .ics file (the whole series). Use only when the user explicitly wants to cancel all future occurrences.
- NEVER use
rmon calendar .ics files directly. Always useevent delete. - After deleting/cancelling, verify with
event listorkhal list.
Managing Contacts
Contacts are stored as vCard (.vcf) files in ~/.openclaw/workspace/contacts/default/ and synced to Migadu CardDAV via vdirsyncer. The send command validates recipients against this contact list.
# List all contacts
$SKILL_DIR/scripts/calendar.sh contact list
# Add a contact (single email)
$SKILL_DIR/scripts/calendar.sh contact add --name "小鹿" --email "mail@luyx.org"
# Add with email type and nickname
$SKILL_DIR/scripts/calendar.sh contact add --name "小橘子" --email "Erica.Jiang@anderson.ucla.edu" --type work --nickname "小橘子"
# Add a second email to an existing contact
$SKILL_DIR/scripts/calendar.sh contact add --name "小橘子" --email "xueweijiang0313@gmail.com" --type home
# Delete a contact
$SKILL_DIR/scripts/calendar.sh contact delete --name "小橘子"
Sending to Contacts
# By name (works when contact has a single email)
$SKILL_DIR/scripts/calendar.sh send --to "小鹿" ...
# By name + type (required when contact has multiple emails)
$SKILL_DIR/scripts/calendar.sh send --to "小橘子:work" ...
# By raw email (must exist in contacts)
$SKILL_DIR/scripts/calendar.sh send --to "Erica.Jiang@anderson.ucla.edu" ...
Replying to Invites
# Accept by himalaya envelope ID
$SKILL_DIR/scripts/calendar.sh reply \
--envelope-id 42 \
--action accept
# Decline with a comment
$SKILL_DIR/scripts/calendar.sh reply \
--envelope-id 42 \
--action decline \
--comment "Sorry, I have a conflict."
# From an .ics file
$SKILL_DIR/scripts/calendar.sh reply \
--ics-file ~/Downloads/meeting.ics \
--action tentative
Reply Options
| Flag | Required | Description |
|---|---|---|
--action |
Yes | accept, decline, or tentative |
--envelope-id |
* | Himalaya envelope ID containing the .ics attachment |
--ics-file |
* | Path to an .ics file (alternative to --envelope-id) |
--from |
No | Your email (default: youlu@luyanxin.com) |
--account |
No | Himalaya account name |
--folder |
No | Himalaya folder (default: INBOX) |
--comment |
No | Optional message to include in reply |
--dry-run |
No | Preview without sending |
* One of --envelope-id or --ics-file is required.
Typical Workflow
- List emails:
himalaya envelope list - Read the invite:
himalaya message read 57 - Reply:
$SKILL_DIR/scripts/calendar.sh reply --envelope-id 57 --action accept
VTODO Tasks
Manage tasks as RFC 5545 VTODO components, stored in ~/.openclaw/workspace/calendars/tasks/ and synced to CalDAV.
Sync Model
The agent's local CalDAV is the source of truth. When a todo is created, it's saved locally and synced to Migadu CalDAV via vdirsyncer — all connected devices (DAVx5, etc.) pick it up automatically. When the user completes a task, they tell the agent, and the agent runs todo complete. The daily todo check cron reads from local files.
Priority Mapping (RFC 5545)
| Label | --priority |
RFC 5545 value |
|---|---|---|
| 高 (high) | high |
1 |
| 中 (medium) | medium |
5 (default) |
| 低 (low) | low |
9 |
todo add — Create a Todo
$SKILL_DIR/scripts/calendar.sh todo add \
--summary "跟进iui保险报销" \
--due "2026-03-25" \
--priority high \
--description "确认iui费用保险报销进度" \
--alarm 1d
| Flag | Required | Description |
|---|---|---|
--summary |
Yes | Todo title |
--due |
No | Due date, YYYY-MM-DD (default: tomorrow) |
--priority |
No | high, medium, or low (default: medium) |
--description |
No | Notes / description |
--alarm |
No | Reminder trigger: 1d, 2h, 30m (default: 1d) |
--dry-run |
No | Preview ICS without saving |
todo list — List Todos
$SKILL_DIR/scripts/calendar.sh todo list # pending only
$SKILL_DIR/scripts/calendar.sh todo list --all # include completed
todo edit — Modify a Todo
$SKILL_DIR/scripts/calendar.sh todo edit --match "保险报销" --due "2026-03-26"
$SKILL_DIR/scripts/calendar.sh todo edit --uid "abc123@openclaw" --priority high
$SKILL_DIR/scripts/calendar.sh todo edit --match "census" --due "2026-03-28" --priority low
| Flag | Required | Description |
|---|---|---|
--uid |
* | Todo UID |
--match |
* | Match on summary text |
--due |
No | New due date (YYYY-MM-DD) |
--priority |
No | New priority: high, medium, or low |
* One of --uid or --match is required. At least one edit flag must be provided.
todo complete — Mark as Done
$SKILL_DIR/scripts/calendar.sh todo complete --uid "abc123@openclaw"
$SKILL_DIR/scripts/calendar.sh todo complete --match "保险报销"
todo delete — Remove a Todo
$SKILL_DIR/scripts/calendar.sh todo delete --uid "abc123@openclaw"
$SKILL_DIR/scripts/calendar.sh todo delete --match "保险报销"
todo check — Daily Digest (Cron)
$SKILL_DIR/scripts/calendar.sh todo check
Same as todo list but only pending items. Exits silently when nothing is pending.
How It Works
Sending invites:
- Generates an RFC 5545 ICS file with
METHOD:REQUEST(viaicalendarlibrary) - Builds a MIME email with a
text/calendarattachment (via Pythonemail.mime) - Sends via
himalaya message send(piped through stdin) - Saves the event to
~/.openclaw/workspace/calendars/home/ - Runs
vdirsyncer syncto push to Migadu CalDAV
Replying to invites:
- Extracts the
.icsattachment from the email (viahimalaya attachment download) - Parses the original event with the
icalendarlibrary - Generates a reply ICS with
METHOD:REPLYand the correctPARTSTAT - Sends the reply to the organizer via
himalaya message send(stdin) - On accept/tentative: saves event to local calendar. On decline: removes it
- Runs
vdirsyncer syncto push changes to Migadu CalDAV
Managing todos:
todo add: Creates a VTODO ICS file (viaicalendarlibrary), saves tocalendars/tasks/, syncs to CalDAVtodo list/complete/delete/check: Delegates totodomanCLI for robust RFC 5545 VTODO parsing- Runs
vdirsyncer syncafter mutations to push changes to Migadu CalDAV
CalDAV sync:
- Events and tasks sync to Migadu and appear on all connected devices (DAVx5, etc.)
- Heartbeat runs
vdirsyncer syncperiodically as a fallback - If sync fails, it warns but doesn't block — next heartbeat catches up
Integration with Email Processor
The email processor (scripts/email_processor/) may classify incoming calendar invites as reminder or confirmation. When reviewing pending emails:
- Check if the email contains a calendar invite (look for
.icsattachment or "calendar" in subject) - If it does, use
replyinstead of the email processor's delete/archive/keep actions - The email processor handles the email lifecycle; this skill handles the calendar response
Checking the Calendar
# List upcoming events (next 7 days)
khal list today 7d
# List events for a specific date
khal list 2026-03-25
# Check for conflicts before sending an invite
khal list 2026-03-25 2026-03-26
Timezone Reference
Common IANA timezones:
America/Los_Angeles— Pacific (default)America/Denver— MountainAmerica/Chicago— CentralAmerica/New_York— EasternAsia/Shanghai— ChinaAsia/Tokyo— JapanEurope/London— UKUTC— Coordinated Universal Time
Troubleshooting
Invite shows as attachment instead of calendar event?
- Ensure the MIME part has
Content-Type: text/calendar; method=REQUEST - Some clients require the
METHOD:REQUESTline in the ICS body
Times are wrong?
- Double-check
--timezonematches the intended timezone - Use ISO 8601 format:
YYYY-MM-DDTHH:MM:SS(no timezone offset in the value)
Event not showing on phone/other devices?
- Run
vdirsyncer syncmanually to force sync - Check
~/.openclaw/workspace/logs/vdirsyncer.logfor errors - Verify the .ics file exists in
~/.openclaw/workspace/calendars/home/
Todos not syncing?
- Check that
~/.openclaw/workspace/calendars/tasks/exists - Verify vdirsyncer has a
cal/taskspair configured - Run
vdirsyncer syncmanually
Recipient doesn't see Accept/Decline?
- Gmail, Outlook, Apple Mail all support
text/calendarmethod=REQUEST - Some webmail clients may vary