{
  "name": "AI Email Summary Workflow",
  "description": "A production-ready daily email digest using Gmail, n8n and Claude or GPT-4o. Fetches only unprocessed emails, truncates safely for large inboxes, summarizes by priority, marks emails as processed, and delivers a structured digest to your inbox. Configured to run reliably for 20-50 emails per day.",
  "version": "2.0",
  "type": "starter_template",
  "kairox_workflow": "ai-email-summary-workflow",
  "stack": {
    "automation": "n8n",
    "email": "Gmail API",
    "ai": "Claude API (recommended) or OpenAI API",
    "output": "Gmail send or Slack"
  },
  "required_credentials": [
    {
      "name": "Gmail OAuth2",
      "type": "oauth2",
      "note": "Required to read, label and send emails. Set up via n8n > Settings > Credentials > New > Gmail OAuth2. Scopes required: gmail.readonly, gmail.labels, gmail.send (or gmail.modify if marking emails as read)."
    },
    {
      "name": "Anthropic API Key",
      "type": "api_key",
      "note": "Used to summarize and prioritize emails. Add via n8n > Settings > Credentials > New > Anthropic API. Swap for OpenAI credentials if using GPT-4o (see node 5 comment)."
    }
  ],
  "timezone_reference": {
    "note": "The schedule trigger uses UTC by default. Find your UTC offset below and adjust the cron hour accordingly.",
    "offsets": {
      "US_Eastern": "UTC-5 (EST) / UTC-4 (EDT) — 7 AM local = cron hour 12 (EST) or 11 (EDT)",
      "US_Central": "UTC-6 (CST) / UTC-5 (CDT) — 7 AM local = cron hour 13 (CST) or 12 (CDT)",
      "US_Pacific": "UTC-8 (PST) / UTC-7 (PDT) — 7 AM local = cron hour 15 (PST) or 14 (PDT)",
      "UK": "UTC+0 (GMT) / UTC+1 (BST) — 7 AM local = cron hour 7 (GMT) or 6 (BST)",
      "Central_Europe": "UTC+1 (CET) / UTC+2 (CEST) — 7 AM local = cron hour 6 (CET) or 5 (CEST)",
      "India": "UTC+5:30 — 7 AM local = cron hour 1 minute 30",
      "Singapore_HK": "UTC+8 — 7 AM local = cron hour 23 (previous UTC day)"
    }
  },
  "nodes": [
    {
      "id": "1",
      "name": "Schedule — Daily Digest",
      "type": "n8n-nodes-base.scheduleTrigger",
      "description": "Runs the digest once per day at your configured morning time.",
      "config": {
        "rule": "0 7 * * *",
        "note": "Default: 7:00 AM UTC. Adjust the hour using the timezone_reference table above. Example for US Eastern (EST): change '7' to '12' → '0 12 * * *'. For n8n cloud, you can also set timezone in workflow settings directly and use local time."
      }
    },
    {
      "id": "2",
      "name": "Gmail — Fetch Unprocessed Emails",
      "type": "n8n-nodes-base.gmail",
      "description": "Fetches unread emails from the last 24 hours that have not already been processed by this workflow. The -label:kairox-summarized filter ensures no email is summarized twice across runs.",
      "config": {
        "operation": "getAll",
        "filters": {
          "q": "is:unread newer_than:1d -label:kairox-summarized"
        },
        "returnAll": false,
        "limit": 50,
        "fields": ["id", "threadId", "from", "subject", "snippet", "internalDate"],
        "note": "The 'kairox-summarized' label must be created manually in Gmail before the first run — Gmail > Settings > Labels > Create new label. Once created, node 6 will apply it automatically to processed emails. This prevents duplicate summaries if the workflow runs twice or fails midway."
      }
    },
    {
      "id": "3",
      "name": "Check — Any Emails to Process",
      "type": "n8n-nodes-base.if",
      "description": "Stops the workflow gracefully if there are no new unprocessed emails. Avoids sending an empty digest or making unnecessary AI calls.",
      "config": {
        "conditions": [
          {
            "field": "{{ $input.all().length }}",
            "operation": "greater_than",
            "value": 0
          }
        ],
        "true_branch": "4",
        "false_branch": "end",
        "note": "The false branch goes to the End node — no digest is sent on empty days. If you want a 'Nothing new today' message, connect the false branch to node 7 (Send Digest) with a static message instead."
      }
    },
    {
      "id": "4",
      "name": "Format + Truncate Emails",
      "type": "n8n-nodes-base.code",
      "description": "Formats all emails into a structured text block for the AI. Applies a 25-email cap and 200-character snippet truncation to keep the payload within safe context window limits.",
      "config": {
        "language": "javascript",
        "code": "const emails = $input.all();\nconst MAX_EMAILS = 25;\nconst MAX_SNIPPET_LENGTH = 200;\n\nconst toProcess = emails.slice(0, MAX_EMAILS);\nconst overflow = emails.length - toProcess.length;\n\nconst formatted = toProcess.map((item, i) => {\n  const e = item.json;\n  const snippet = (e.snippet || '').slice(0, MAX_SNIPPET_LENGTH);\n  const truncated = (e.snippet || '').length > MAX_SNIPPET_LENGTH ? '...' : '';\n  const date = e.internalDate\n    ? new Date(parseInt(e.internalDate)).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })\n    : 'unknown time';\n  return `--- Email ${i + 1} ---\\nFrom: ${e.from || 'unknown'}\\nSubject: ${e.subject || '(no subject)'}\\nReceived: ${date}\\nPreview: ${snippet}${truncated}`;\n}).join('\\n\\n');\n\nconst header = `Inbox summary request — ${toProcess.length} email${toProcess.length !== 1 ? 's' : ''} to process${overflow > 0 ? ` (${overflow} additional emails not included — inbox volume exceeded daily limit)` : ''}`;\n\n// Pass email IDs through for label application in node 6\nconst emailIds = toProcess.map(item => item.json.id).filter(Boolean);\n\nreturn [{\n  json: {\n    emailCount: toProcess.length,\n    overflow,\n    emailIds,\n    formattedContent: `${header}\\n\\n${formatted}`\n  }\n}];"
      }
    },
    {
      "id": "5",
      "name": "Claude — Summarize and Prioritize",
      "type": "n8n-nodes-base.anthropic",
      "description": "Sends the formatted email list to Claude and returns a structured priority digest.",
      "config": {
        "model": "claude-3-5-haiku-20241022",
        "max_tokens": 1500,
        "system_prompt": "You are an inbox intelligence system. Your task is to process a list of emails and produce a structured morning digest for a busy professional.\n\nYour output must follow this exact format:\n\n## Priority Actions\nEmails requiring a response or decision today. For each item: sender name, subject, and one sentence on what action is needed. If none, write 'None today.'\n\n## Important — Read When Ready\nEmails worth reading but not requiring immediate action. For each: sender name, subject, one sentence on why it matters. If none, omit this section.\n\n## FYI / Low Priority\nEmails to be aware of but requiring no action. Group similar senders (e.g. '3 newsletters: Morning Brew, Lenny's Newsletter, The Hustle'). Be brief.\n\n## Summary\nOne paragraph (3–5 sentences): total email count, the single most important thing to act on first, and any unusual patterns or flags worth noting.\n\nRULES:\n- If an email is promotional, automated, or a newsletter with no personal relevance, place it in FYI\n- If an email is from a real person addressing you directly, default to Priority or Important\n- Use actual names, company names, and subject lines — be specific\n- Do not fabricate context you cannot see in the email data\n- If the subject is empty, use the sender name and preview to infer the topic\n- Keep the digest readable in under 2 minutes",
        "user_message": "{{ $json.formattedContent }}",
        "note": "Claude 3.5 Haiku is recommended — fast, cost-effective, and reliable for this use case. To use GPT-4o instead: replace this node with the OpenAI node, set model to 'gpt-4o' or 'gpt-4o-mini', and use the same system_prompt and user_message values."
      }
    },
    {
      "id": "6",
      "name": "Gmail — Apply Processed Label",
      "type": "n8n-nodes-base.gmail",
      "description": "Applies the 'kairox-summarized' label to all emails processed in this run. This ensures the next run's Gmail query excludes them, preventing duplicate summaries.",
      "config": {
        "operation": "addLabels",
        "label_names": ["kairox-summarized"],
        "message_ids": "{{ $('Format + Truncate Emails').item.json.emailIds }}",
        "continue_on_error": true,
        "note": "Set 'Continue On Error' to true — if label application fails, the digest is still delivered. The label must exist in Gmail before this node runs. Create it manually: Gmail > Settings > Labels > Create new label > 'kairox-summarized'."
      }
    },
    {
      "id": "7",
      "name": "Send Digest",
      "type": "n8n-nodes-base.gmail",
      "description": "Sends the AI-generated digest to your configured delivery address.",
      "config": {
        "operation": "send",
        "to": "YOUR_EMAIL@example.com",
        "subject": "Morning Digest — {{ $now.setZone('America/New_York').toFormat('EEE MMM d') }}",
        "body_type": "html",
        "body": "{{ $('Claude - Summarize and Prioritize').item.json.text.replace(/\\n/g, '<br>').replace(/## /g, '<h3>').replace(/<h3>(.*?)<br>/g, '<h3>$1</h3>') }}",
        "note": "Replace 'YOUR_EMAIL@example.com' with your actual delivery address — this is where the digest arrives each morning. Using the same Gmail address you're reading from works fine. Update the timezone in the subject line (America/New_York → your timezone identifier) so the date label is correct for your location."
      }
    },
    {
      "id": "8",
      "name": "Error — Gmail Failure",
      "type": "n8n-nodes-base.noOp",
      "description": "Error handler for Gmail API failures (node 2 or 7). Connect via n8n's error workflow or 'Continue On Error' to capture and log failures without silent drops.",
      "config": {
        "note": "In production: replace this with a Slack message node or a second Gmail send node using a backup address. To activate: in n8n workflow settings, set an Error Workflow and connect it to this node type. The error will include the failed node name and error message."
      }
    },
    {
      "id": "9",
      "name": "Error — AI Failure",
      "type": "n8n-nodes-base.noOp",
      "description": "Error handler for Claude or OpenAI API failures (node 5). Prevents silent failures when the AI call times out or returns an error.",
      "config": {
        "note": "In production: connect this to a Slack alert or a fallback email sending the raw unformatted email list without AI summarization. To trigger: enable 'Continue On Error' on node 5 and connect the error output to this node."
      }
    }
  ],
  "connections": [
    { "from": "1", "to": "2", "label": "daily schedule" },
    { "from": "2", "to": "3", "label": "fetched emails" },
    { "from": "3", "to": "4", "branch": "true", "label": "emails found" },
    { "from": "3", "to": "end", "branch": "false", "label": "no emails — stop" },
    { "from": "4", "to": "5", "label": "formatted content" },
    { "from": "5", "to": "6", "label": "AI digest complete" },
    { "from": "6", "to": "7", "label": "label applied" },
    { "from": "2", "to": "8", "branch": "error", "label": "Gmail fetch error" },
    { "from": "5", "to": "9", "branch": "error", "label": "AI API error" }
  ],
  "setup_checklist": [
    "1. Add Gmail OAuth2 credentials in n8n > Settings > Credentials. Required scopes: gmail.readonly, gmail.labels, gmail.send",
    "2. Add your Anthropic API key (or OpenAI API key) as a credential in n8n",
    "3. Create a Gmail label named exactly 'kairox-summarized' — Gmail > Settings > See all settings > Labels > Create new label",
    "4. Replace 'YOUR_EMAIL@example.com' in node 7 with your actual delivery address",
    "5. Adjust the cron expression in node 1 to your local time using the timezone_reference table in this file",
    "6. Update the timezone identifier in the node 7 subject line to match your location (e.g. 'Europe/London', 'Asia/Singapore')",
    "7. Run the workflow manually once with a small inbox to validate the full pipeline — check that the label is applied and the digest arrives",
    "8. Optionally: set an n8n Error Workflow (workflow settings) to activate nodes 8 and 9 for production monitoring"
  ],
  "production_notes": [
    "Node 2 query '-label:kairox-summarized' prevents duplicate summaries — the same email is never processed twice across runs",
    "Node 3 (empty inbox check) stops execution gracefully with no output when there are no new emails — no empty digest, no wasted API calls",
    "Node 4 caps at 25 emails and 200 chars per snippet — this keeps the AI prompt under ~8,000 tokens, well within safe context for all major models",
    "Node 6 (label application) has continue_on_error = true — if labelling fails, the digest is still delivered and you only lose deduplication for that run",
    "The digest is sent as plain HTML with basic heading formatting — most email clients render it cleanly; adjust the body expression in node 7 for richer styling",
    "For Slack output: replace node 7 with a Slack node using the 'Chat Message' operation and the same body content"
  ]
}
