Gmail via Composio

The complete gogcli-to-Composio mapping. Draft-first workflow. Multi-account. Reply-all. Tested & verified.

Skill gmail-composio-guide — Version 1.0.0 — Kelvin / Hermes

1. Account & Health

bash -lc 'set -a; . ~/.hermes/.env; set +a; composio-session health'
bash -lc 'set -a; . ~/.hermes/.env; set +a; composio-session accounts list --toolkit gmail'

2. Critical: JSON Payload Format

Always write the payload to a temp file and use $(cat /tmp/payload.json) to avoid shell escaping hell.

import subprocess, json
payload = json.dumps({"query": "in:inbox", "max_results": 10})
with open('/tmp/gmail_payload.json', 'w') as f:
    f.write(payload)
cmd = "bash -lc 'set -a; . ~/.hermes/.env; set +a; composio-session tools execute GMAIL_FETCH_EMAILS --toolkit gmail --account primary-gmail --json \"$(cat /tmp/gmail_payload.json)\"'"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)

3. Search & Read

Query patterns (Gmail search syntax):

Parameters:

4. Default Workflow: Draft First, Send After Approval

The Hermes charter requires explicit same-conversation approval before sending.
  1. Create draft with GMAIL_CREATE_EMAIL_DRAFT
  2. Present to user in chat with draft URL
  3. User reviews in Gmail Drafts folder
  4. User says "send it" — call GMAIL_SEND_DRAFT with the draft_id

Only skip the draft step for trivial internal emails where the user has pre-approved sending in the same conversation.

5. Create Draft

composio-session tools execute GMAIL_CREATE_EMAIL_DRAFT \
  --toolkit gmail --account primary-gmail \
  --json '{"recipient_email":"a@example.com","subject":"Subject","body":"Message","is_html":false}'

Draft parameters:

FieldTypeNotes
recipient_emailstringPrimary To recipient (optional for drafts)
extra_recipientsarrayAdditional To recipients [ ]
ccarrayCC recipients [ ]
bccarrayBCC recipients [ ]
subjectstringOptional for drafts
bodystringPlain text or HTML
is_htmlbooleanSet true for HTML body
thread_idstringReply to existing thread (leave subject empty!)
attachmentobjectRequires S3 key from prior upload
CRITICAL: CC/BCC/Extra Recipients are arrays, not comma-separated strings.
draft_payload = {
    "recipient_email": "primary@example.com",
    "extra_recipients": ["jane@example.com", "John Doe <john@example.com>"],
    "cc": ["cc1@example.com", "cc2@example.com"],
    "bcc": ["bcc@example.com"],
    "subject": "Subject",
    "body": "Message body",
    "is_html": False
}

Thread replies: Provide thread_id and leave subject empty to stay in the same thread. Setting a subject creates a new thread.

6. List / Send / Update / Delete Drafts

# List drafts
composio-session tools execute GMAIL_LIST_DRAFTS \
  --toolkit gmail --account primary-gmail \
  --json '{"max_results":10,"verbose":true}'

# Send draft (AS-IS — no recipient override)
composio-session tools execute GMAIL_SEND_DRAFT \
  --toolkit gmail --account primary-gmail \
  --json '{"draft_id":"r-1234567890"}'

# Update draft (replaces ALL content)
composio-session tools execute GMAIL_UPDATE_DRAFT \
  --toolkit gmail --account primary-gmail \
  --json '{"draft_id":"r-1234567890","recipient_email":"...","subject":"...","body":"..."}'

# Delete draft (permanent)
composio-session tools execute GMAIL_DELETE_DRAFT \
  --toolkit gmail --account primary-gmail \
  --json '{"draft_id":"r-1234567890","user_id":"me"}'

7. Send Directly (Bypass Draft)

composio-session tools execute GMAIL_SEND_EMAIL \
  --toolkit gmail --account primary-gmail \
  --json '{"recipient_email":"a@example.com","subject":"Subject","body":"Message"}'

Only use when user has explicitly pre-approved sending, or for trivial/internal emails.

8. Replies & Reply-All

Composio has no native reply/reply-all tool. You must manually fetch headers and construct the draft.

Step 1: Fetch Original Message Headers

composio-session tools execute GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID \
  --toolkit gmail --account primary-gmail \
  --json '{"message_id":"19b11732c1b578fd","format":"metadata"}'

Step 2: Parse Headers (Python)

def extract_headers(msg_data):
    headers = {h["name"].lower(): h["value"] for h in msg_data["payload"]["headers"]}
    return {
        "from": headers.get("from"),
        "to": headers.get("to"),
        "cc": headers.get("cc", ""),
        "subject": headers.get("subject"),
        "thread_id": msg_data.get("threadId", ""),
        "message_id": headers.get("message-id", "")
    }

def parse_emails(header_value):
    if not header_value: return []
    import re
    pattern = r'(?:[^<>,]*?)\s*<([^>]+)>|([^\s<>,]+@[^\s<>,]+)'
    matches = re.findall(pattern, header_value)
    return [m[0] if m[0] else m[1] for m in matches]

Step 3: Build Reply-All Draft

from_email = "kelvin@iimmpact.com"
original_sender = parse_emails(headers["from"])[0]
original_to = parse_emails(headers["to"])
original_cc = parse_emails(headers["cc"])

reply_to = [original_sender]
reply_to += [e for e in original_to if e.lower() != from_email.lower()]
reply_cc = [e for e in original_cc if e.lower() != from_email.lower()]

reply_all_draft = {
    "thread_id": headers["thread_id"],
    "recipient_email": reply_to[0],
    "extra_recipients": reply_to[1:] if len(reply_to) > 1 else [],
    "cc": reply_cc,
    "subject": "",  # Leave empty to stay in same thread
    "body": "Reply-all text here",
    "is_html": False
}
Important: Leave subject empty when using thread_id. Setting a subject creates a new thread. Threading is handled by thread_id — Gmail auto-sets In-Reply-To/References.

9. HTML Emails

html_draft = {
    "recipient_email": "a@example.com",
    "subject": "Subject",
    "body": "<p>Hello,</p><ol><li>First</li><li>Second</li></ol>...",
    "is_html": True
}

10. Multi-Account / Profile Switching

# Authorize multiple accounts
composio-session authorize gmail --alias primary-gmail --set-default
composio-session authorize gmail --alias secondary-gmail

# Set default
composio-session account default --toolkit gmail --account secondary-gmail

# Rename alias
composio-session account rename --toolkit gmail --from primary-gmail --to iimmpact-gmail
Critical finding (tested 2026-05-23):
display_url always points to mail.google.com/mail/u/0/#... — the first browser account, regardless of which --account you used.

Test: Draft created with --account secondary-gmail → URL: u/0/#drafts/r-5855707778456175488 (NOT u/1/).

Don't rely on display_url for multi-account. Tell the user which account alias was used.

11. Common Pitfalls

  1. Default max_results is 1 — Always set explicitly.
  2. Results NOT sorted by recency — Sort by internalDate client-side.
  3. Payload may be null — Use GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID for guaranteed content.
  4. Base64url encoding — Replace -+, _/, fix padding.
  5. Label IDs vs label nameslabel_ids requires internal IDs (e.g. "Label_123").
  6. Date queries are UTC whole days — Adjust for timezone.
  7. Shell escaping JSON — Never inline JSON with quotes. Use temp file pattern.
  8. CC/BCC/Extra Recipients are arrays["a@x.com", "b@x.com"], not comma-separated strings.
  9. Thread reply: leave subject empty — Setting a subject creates a new thread.
  10. GMAIL_SEND_DRAFT cannot add recipients — Draft MUST already have recipients.
  11. GMAIL_UPDATE_DRAFT replaces everything — Not a patch. Provide complete fields.
  12. Newly created drafts may not appear immediately — Allow brief delay.
  13. Draft IDs differ from message IDs — Draft IDs have r prefix. Don't interchange.

12. Tool Discovery

composio-session tools search "" --toolkits gmail
composio-session tools search "send" --toolkits gmail
composio-session tools search "draft" --toolkits gmail
composio-session tools search "label" --toolkits gmail
composio-session tools search "reply" --toolkits gmail

Generated from Hermes skill gmail-composio-guide · Last updated 2026-05-23