Read ../config/user.json (resolves to ~/executive-assistant-skills/config/user.json).
Extract and use throughout:
primary_email, work_email — Gmail accountswhatsapp — for notification deliveryworkspace — absolute path to OpenClaw workspacesignature — email signatureRead ../config/DEBUG_LOGGING.md for the full convention. Use python3 {user.workspace}/scripts/skill_log.py todoist-due-drafts <level> "<message>" ['<details>'] at every key step. Log BEFORE and AFTER every external call (todoist-cli, gog, mcporter). On any error, log the full command and stderr before continuing.
source {user.workspace}/.env
todoist-cli today --json
Also check overdue tasks:
todoist-cli list --filter "overdue" --json
Filter for tasks whose content matches outreach intent:
ping, email, follow up, follow-up, send, reach out, text, message, intro, connect, check in, nudge, reply, respond, draft
Skip tasks that are clearly NOT outreach (e.g. "review doc", "read report", "build proposal").
Read and follow ~/executive-assistant-skills/email-drafting/SKILL.md for all drafting rules.
For each task:
# Find the meeting in Granola
mcporter call granola list_meetings --args '{"time_range": "custom", "custom_start": "<meeting-date>", "custom_end": "<meeting-date+1>"}'
# Query for what was discussed with this person
mcporter call granola query_granola_meetings --args '{"query": "What did {user.name} discuss with <recipient> and what did he promise or commit to do?", "document_ids": ["<meeting_id>"]}'
Then cross-check with Grain for the full transcript:
mcporter call grain.list_attended_meetings --args '{}'
# Note: Grain's schema does not support `start_date`/`end_date` filters. Call with empty args and filter the results manually by `start_datetime` to match the meeting date range.
mcporter call grain.fetch_meeting_transcript --args '{"meeting_id": "<grain_meeting_id>"}'
When meeting context is available, the email draft must reflect what was actually said — not just the task title. Look for: specific commitments, timelines discussed, names/projects mentioned, tone of the conversation, and any docs/links promised.{user.work_email} for professional/business contacts or {user.primary_email} for personal contacts. When ambiguous, use {user.work_email}.--thread-id to keep it in the same thread.{user.signature}
Send a single WhatsApp message to {user.whatsapp} with:
📬 *Due-today drafts*
<N> email drafts created from today's Todoist tasks:
1. **<recipient>** — <one-line intent> (draft in <account>)
2. ...
Review and send when ready.
If tasks exist but none are outreach-type, report:
📋 *Due-today tasks (no drafts needed)*
<list of today's tasks — quick reference>
If no tasks due today → NO_REPLY (skip notification entirely).
python3 {user.workspace}/scripts/cron_canary.py ping todoist-due-drafts
todoist-cli fails: notify via WhatsApp "⚠️ Todoist due-drafts failed: .env file or token expired: report "⚠️ Todoist token unavailable — check agent-secrets lease" and exit.gog --account <account> --no-input gmail search "to:<recipient> in:sent newer_than:14d" --json. If recent sent mail exists in the same thread, skip drafting and note "already replied" in the notification.gog --account <account> --no-input gmail drafts --json. If a draft already exists for the same thread (matching thread ID or recipient + subject), skip and note "draft already exists."