{
  "id": "XKjbEok8gd4827Ki",
  "name": "Weekly Client Report Generator",
  "tags": [],
  "nodes": [
    {
      "id": "7fab9d22-a884-4911-b7c5-16d34f972703",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -592,
        -64
      ],
      "parameters": {
        "color": 2,
        "width": 656,
        "height": 880,
        "content": "# Weekly Client Report Generator\n\nEvery Friday morning, pulls this week's data from Google Sheets and sends each client a professional, narrative-style weekly update not a bullet list of tasks, but a clear, confident summary a senior account manager would write.\n\n\n## How it works\n\n1. **Every Friday at 9 AM** the schedule trigger fires.\n2. **Fetch Weekly Data** reads all active client rows from your Google Sheet.\n3. **Normalize Sheet Data** maps the row columns into a consistent shape and builds the week label automatically.\n4. **Claude** writes a 3-4 paragraph narrative report with an HTML and plain-text version; professional prose, not a task dump.\n5. **Send Report Email** delivers the HTML email directly to the client's email address on record.\n\n\n## Google Sheet setup\n\nCreate a sheet with these exact column headers:\n\n\n| Column | Required | Notes |\n| :--- | :---: | :--- |\n| Client Name | Yes | Recipient name |\n| Client Email | Yes | Where the report is sent |\n| Project Name | No | Shown in report heading |\n| Sender Name | No | Your name / team name |\n| Sender Email | No | Optional reply-to |\n| Status | No | Set to \"paused\" or \"inactive\" to skip |\n| Tasks Completed | No | Comma-separated or paragraph |\n| Tasks In Progress | No | Optional |\n| Blockers | No | Optional |\n| Next Week Plan | No | Optional |\n| Metric: [Name] | No | Add as many as you like (e.g., Metric: Sessions) |\n\n\nFor metrics, add any column starting with \"Metric:\" e.g. \"Metric: Revenue\", \"Metric: Tasks Closed\", \"Metric: Uptime\". All matching columns are included automatically.\n\n\n## Setup steps\n\n- [ ] Create a Google Sheet with the columns above and paste the Sheet ID into the Fetch Weekly Data node\n- [ ] Connect Google Sheets credentials\n- [ ] Add Anthropic credentials to the Anthropic Chat Model node\n- [ ] Connect Gmail credentials for sending reports\n- [ ] Customize the Claude system prompt tone to match your agency's voice"
      },
      "typeVersion": 1
    },
    {
      "id": "15ed4ca5-98b6-4ff8-b896-b4e9cf850ab6",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        96,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 624,
        "content": "## Fetch and normalize\n\nSchedule fires every Friday. Reads all active client rows and builds the reporting week label automatically."
      },
      "typeVersion": 1
    },
    {
      "id": "ea718811-4cb6-4438-ad83-b232626469d4",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        816,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 624,
        "content": "## AI report generation\n\nClaude/ChatGPT/.... writes a narrative report 3-4 paragraphs of professional prose, not a task list. Returns HTML and plain text."
      },
      "typeVersion": 1
    },
    {
      "id": "8def53e1-5a5b-4483-9893-c5f5f3ca2590",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1424,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 624,
        "content": "## Send\n\nHTML email delivered to each client's email address. Runs for every active row in the sheet."
      },
      "typeVersion": 1
    },
    {
      "id": "a5d50954-95af-452c-82ca-c4f5a69fef3c",
      "name": "Every Friday at 9 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        144,
        176
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * 5"
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "8eb5c5ee-6fbf-4cff-aa4a-890308d680ce",
      "name": "Fetch Weekly Data",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        384,
        176
      ],
      "parameters": {
        "operation": "getRows",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_YOUR_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "198cb470-11ee-4e69-91ec-e901a6a04f24",
      "name": "Normalize Sheet Data",
      "type": "n8n-nodes-base.code",
      "position": [
        624,
        176
      ],
      "parameters": {
        "jsCode": "// Normalize rows from Google Sheets into the shape the AI prompt needs\n// Expects one row per client with specific column headers (see setup steps)\n\nconst rows = $input.all().map(i => i.json);\nconst results = [];\n\nfor (const row of rows) {\n  if (!row['Client Name'] || !row['Client Email']) continue;\n  if (row['Status'] === 'paused' || row['Status'] === 'inactive') continue;\n\n  // Build metrics text from any column that starts with \"Metric:\"\n  const metricLines = [];\n  for (const [key, val] of Object.entries(row)) {\n    if (key.startsWith('Metric:') && val) {\n      const metricName = key.replace('Metric:', '').trim();\n      metricLines.push(`${metricName}: ${val}`);\n    }\n  }\n\n  // Get current week label\n  const now = new Date();\n  const weekStart = new Date(now);\n  weekStart.setDate(now.getDate() - now.getDay() + 1); // Monday\n  const weekEnd = new Date(weekStart);\n  weekEnd.setDate(weekStart.getDate() + 6);\n  const fmt = (d) => d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });\n  const weekLabel = `${fmt(weekStart)} \u2013 ${fmt(weekEnd)}`;\n\n  results.push({\n    json: {\n      client_name: row['Client Name'],\n      client_email: row['Client Email'],\n      project_name: row['Project Name'] || 'Your Project',\n      sender_name: row['Sender Name'] || 'Your Team',\n      sender_email: row['Sender Email'] || null,\n      week_label: weekLabel,\n      metrics_text: metricLines.join('\\n') || 'No metrics provided this week',\n      tasks_completed: row['Tasks Completed'] || 'No tasks logged',\n      tasks_in_progress: row['Tasks In Progress'] || null,\n      blockers: row['Blockers'] || null,\n      next_week: row['Next Week Plan'] || null\n    }\n  });\n}\n\nif (results.length === 0) {\n  throw new Error('No active client rows found in the sheet. Check that rows have Client Name, Client Email, and Status is not \"paused\" or \"inactive\".');\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "e5da3f70-a43e-4b30-87fc-d3fcbbccd756",
      "name": "Generate Report",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        864,
        176
      ],
      "parameters": {
        "text": "=Write a weekly client report based on this data.\n\nClient name: {{ $json.client_name }}\nProject name: {{ $json.project_name }}\nReporting period: {{ $json.week_label }}\nSender name (your name): {{ $json.sender_name }}\n\nMetrics this week:\n{{ $json.metrics_text }}\n\nTasks completed:\n{{ $json.tasks_completed }}\n\nTasks in progress:\n{{ $json.tasks_in_progress || 'None noted' }}\n\nBlockers or issues:\n{{ $json.blockers || 'None' }}\n\nNext week plan:\n{{ $json.next_week || 'To be confirmed' }}\n\nWrite a professional narrative report. Do NOT use bullet points. Return ONLY valid JSON.",
        "options": {
          "systemMessage": "You are a professional account manager writing weekly client progress reports for a freelance agency or consultant. You turn raw numbers and task data into a clear, confident narrative that makes clients feel informed and confident in the team's progress.\n\n## Tone rules\n- Professional but warm \u2014 not corporate, not casual\n- Lead with the most important insight or highlight, not a list of tasks\n- If numbers are down or tasks were incomplete, acknowledge it briefly and pivot to context or next steps \u2014 never hide it, never over-explain it\n- 3-4 short paragraphs max. Each paragraph has one focus.\n- Never use bullet points in the narrative \u2014 this is prose\n- End with a clear forward-looking statement: what happens next week"
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3
    },
    {
      "id": "be400938-f3d6-451a-acc9-09090b9e55bd",
      "name": "Anthropic Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        864,
        384
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-5-20250929",
          "cachedResultName": "Claude Sonnet 4.5"
        },
        "options": {
          "temperature": 0.5
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "cd350600-31af-44b4-a00b-227abe9b871a",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1104,
        384
      ],
      "parameters": {
        "autoFix": true,
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"email_subject\": {\n      \"type\": \"string\"\n    },\n    \"report_html\": {\n      \"type\": \"string\",\n      \"description\": \"Full HTML email body with inline styles, ready to send\"\n    },\n    \"report_plain\": {\n      \"type\": \"string\",\n      \"description\": \"Plain text version of the same report\"\n    }\n  },\n  \"required\": [\n    \"email_subject\",\n    \"report_html\",\n    \"report_plain\"\n  ],\n  \"additionalProperties\": false\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "d2e2ffee-7aff-4994-b93a-65f690425bd0",
      "name": "Send Report Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1504,
        176
      ],
      "parameters": {
        "sendTo": "={{ $('Normalize Sheet Data').item.json.client_email }}",
        "message": "={{ $input.first().json.output?.report_html || $input.first().json.report_html }}",
        "options": {
          "appendAttribution": false
        },
        "subject": "={{ $input.first().json.output?.email_subject || $input.first().json.email_subject }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "4ee9a7f9-a49f-46c3-86c6-263003421c77",
  "connections": {
    "Generate Report": {
      "main": [
        [
          {
            "node": "Send Report Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Weekly Data": {
      "main": [
        [
          {
            "node": "Normalize Sheet Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anthropic Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Generate Report",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Structured Output Parser",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Every Friday at 9 AM": {
      "main": [
        [
          {
            "node": "Fetch Weekly Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Sheet Data": {
      "main": [
        [
          {
            "node": "Generate Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Generate Report",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    }
  }
}