How to Build a Weekly Customer Engagement Summary Agent with n8n and HubSpot (Free Template)

How to Build a Weekly Customer Engagement Summary Agent with n8n and HubSpot (Free Template)

Sales and marketing teams waste hours each week manually compiling customer engagement data from HubSpot. This n8n workflow automates the entire process—pulling engagement metrics, generating formatted summaries, and posting them directly to Microsoft Teams. You'll learn how to build this automation yourself, with a complete JSON template at the end.

The Problem: Manual Customer Engagement Reporting

Current challenges:

  • Sales teams manually export HubSpot data every week to track customer interactions
  • Marketing managers spend 3-5 hours compiling engagement metrics across multiple contacts
  • Teams miss critical engagement patterns because data sits in HubSpot instead of communication channels
  • Weekly reports arrive late or inconsistently, delaying strategic decisions

Business impact:

  • Time spent: 3-5 hours per week per team member
  • Delayed insights: 2-3 day lag between engagement and team awareness
  • Missed opportunities: High-value prospects slip through due to lack of visibility

Manual reporting doesn't scale. As your customer base grows, the time required increases linearly while the value of each individual report decreases.

The Solution Overview

This n8n workflow connects HubSpot's CRM data directly to Microsoft Teams through automated weekly summaries. The workflow retrieves customer engagement data, processes it into readable summaries, and posts formatted updates to your team channel every Monday morning.

Key components include HubSpot API integration for data retrieval, n8n's scheduling capabilities for weekly automation, data transformation nodes for formatting, and Microsoft Teams webhooks for delivery. The entire process runs without human intervention once configured.

What You'll Build

Component Technology Purpose
Data Source HubSpot CRM API Retrieve customer engagement metrics and contact data
Scheduling n8n Schedule Trigger Execute workflow every Monday at 9 AM
Data Processing n8n Function Nodes Transform raw HubSpot data into readable summaries
Notification Delivery Microsoft Teams Webhook Post formatted summaries to team channel
Error Handling n8n Error Workflow Catch and log failures for troubleshooting

Capabilities:

  • Automatically pulls last 7 days of customer engagements from HubSpot
  • Aggregates email opens, link clicks, meeting bookings, and deal updates
  • Formats data into structured summaries with key metrics highlighted
  • Posts to designated Teams channel every Monday morning
  • Handles API rate limits and connection failures gracefully
  • Scales to handle 100+ customer records per execution

Prerequisites

Before starting, ensure you have:

  • n8n instance (cloud or self-hosted version 0.220.0+)
  • HubSpot account with API access (Marketing Hub Professional or higher)
  • Microsoft Teams with webhook permissions for target channel
  • HubSpot Private App or API key with crm.objects.contacts.read and crm.objects.deals.read scopes
  • Basic understanding of REST APIs and JSON data structures

Step 1: Configure HubSpot API Access

Before building the workflow, you need proper HubSpot API credentials. This ensures your n8n workflow can retrieve customer engagement data securely.

Create HubSpot Private App:

  1. Navigate to HubSpot Settings → Integrations → Private Apps
  2. Click "Create a private app"
  3. Name it "n8n Customer Engagement Automation"
  4. Under Scopes, enable: crm.objects.contacts.read, crm.objects.deals.read, crm.objects.companies.read
  5. Copy the generated access token (you'll need this in n8n)

In n8n, add HubSpot credentials:

  1. Go to Credentials → New Credential
  2. Select "HubSpot API"
  3. Choose "Private App Token" authentication
  4. Paste your access token
  5. Test the connection to verify access

Why this works:
Private Apps provide granular permission control without exposing your entire HubSpot account. The read-only scopes ensure the workflow cannot modify customer data, reducing security risk.

Step 2: Set Up Microsoft Teams Webhook

Microsoft Teams uses incoming webhooks to receive messages from external applications. You'll configure one webhook for your target channel.

Create Teams webhook:

  1. Open Microsoft Teams and navigate to your target channel
  2. Click the three dots (···) next to the channel name
  3. Select "Connectors" from the menu
  4. Search for "Incoming Webhook" and click Configure
  5. Name it "HubSpot Engagement Summaries"
  6. Upload an icon (optional) and click Create
  7. Copy the webhook URL (starts with https://outlook.office.com/webhook/)

In n8n, add Teams credentials:

  1. Go to Credentials → New Credential
  2. Select "HTTP Request" (Teams uses standard webhooks)
  3. Authentication: None (webhook URL contains security token)
  4. You'll paste the webhook URL directly into the HTTP Request node

Common issues:

  • Webhook URL expires if connector is removed → Save the URL securely
  • Channel permissions required → Ensure you're a channel owner or have connector permissions

Step 3: Build the Weekly Schedule Trigger

The Schedule Trigger node starts your workflow automatically every Monday morning.

Configure Schedule node:

  1. Add a "Schedule Trigger" node as your workflow's starting point
  2. Set Trigger Interval: "Weeks"
  3. Set Weeks Between Triggers: 1
  4. Set Trigger on Weekday: "Monday"
  5. Set Trigger at Hour: 9
  6. Set Trigger at Minute: 0
  7. Set Timezone: Your business timezone (e.g., "America/New_York")
{
  "rule": {
    "interval": [{
      "field": "weeks",
      "triggerAtHour": 9,
      "triggerAtMinute": 0,
      "triggerOnWeekday": 1
    }]
  }
}

Why this approach:
Monday morning execution ensures teams see engagement summaries at the start of their work week. The 9 AM timing allows for review during morning planning sessions. Weekly frequency balances actionable insights with information overload.

Variables to customize:

  • triggerOnWeekday: Change to 0 (Sunday) through 6 (Saturday) for different days
  • triggerAtHour: Adjust for your team's schedule (use 24-hour format)
  • interval: Switch to "Days" for daily summaries if needed

Step 4: Retrieve HubSpot Contact Engagements

This section pulls customer engagement data from HubSpot for the past 7 days.

Add HubSpot node:

  1. Add "HubSpot" node after the Schedule Trigger
  2. Set Credential: Select your HubSpot credential from Step 1
  3. Set Resource: "Contact"
  4. Set Operation: "Get All"
  5. Enable "Return All" to retrieve all matching contacts
  6. Add Filter: lastmodifieddate is after {{$now.minus({days: 7}).toISO()}}

Node configuration:

{
  "resource": "contact",
  "operation": "getAll",
  "returnAll": true,
  "filters": {
    "filterGroups": [{
      "filters": [{
        "propertyName": "lastmodifieddate",
        "operator": "GT",
        "value": "{{$now.minus({days: 7}).toISO()}}"
      }]
    }]
  },
  "properties": [
    "email",
    "firstname",
    "lastname",
    "hs_email_open",
    "hs_email_click",
    "notes_last_updated",
    "num_associated_deals"
  ]
}

Why this works:
The lastmodifieddate filter ensures you only retrieve contacts with recent activity, reducing API calls and processing time. Requesting specific properties instead of all fields minimizes payload size and speeds up execution.

Additional properties to consider:

  • hs_analytics_num_page_views: Track website engagement
  • hs_email_last_email_name: Identify which campaigns drove engagement
  • hs_lead_status: Segment by lead qualification stage

Step 5: Retrieve Associated Deals

Customer engagements often correlate with deal progression. This step enriches contact data with deal information.

Add second HubSpot node:

  1. Add another "HubSpot" node after the contact retrieval
  2. Set Resource: "Deal"
  3. Set Operation: "Get All"
  4. Enable "Return All"
  5. Add Filter: closedate is after {{$now.minus({days: 7}).toISO()}} OR hs_lastmodifieddate is after {{$now.minus({days: 7}).toISO()}}

Node configuration:

{
  "resource": "deal",
  "operation": "getAll",
  "returnAll": true,
  "filters": {
    "filterGroups": [{
      "filters": [{
        "propertyName": "hs_lastmodifieddate",
        "operator": "GT",
        "value": "{{$now.minus({days: 7}).toISO()}}"
      }]
    }]
  },
  "properties": [
    "dealname",
    "amount",
    "dealstage",
    "closedate",
    "pipeline",
    "hs_lastmodifieddate"
  ]
}

Why this approach:
Filtering by hs_lastmodifieddate captures deals that moved stages, had amount changes, or received updates—all signals of active sales engagement. The 7-day window matches your contact engagement timeframe for consistent reporting.

Step 6: Transform Data into Summary Format

Raw HubSpot data needs formatting before posting to Teams. This Function node aggregates metrics and creates readable text.

Add Function node:

  1. Add "Function" node after both HubSpot nodes
  2. Set Mode: "Run Once for All Items"
  3. Paste the transformation code below
// Aggregate contact engagement metrics
const contacts = $input.first().json;
const deals = $input.last().json;

const summary = {
  totalContacts: contacts.length,
  emailOpens: contacts.reduce((sum, c) => sum + (c.hs_email_open || 0), 0),
  emailClicks: contacts.reduce((sum, c) => sum + (c.hs_email_click || 0), 0),
  activeDeals: deals.length,
  totalDealValue: deals.reduce((sum, d) => sum + (parseFloat(d.amount) || 0), 0),
  closedDeals: deals.filter(d => d.dealstage === 'closedwon').length
};

// Format currency
const formatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  minimumFractionDigits: 0
});

// Build Teams message
const message = {
  "@type": "MessageCard",
  "@context": "https://schema.org/extensions",
  "summary": "Weekly Customer Engagement Summary",
  "themeColor": "0078D4",
  "title": "📊 Weekly Customer Engagement Summary",
  "sections": [{
    "activityTitle": `Week of ${$now.minus({days: 7}).toFormat('MMM dd')} - ${$now.toFormat('MMM dd, yyyy')}`,
    "facts": [
      { "name": "Active Contacts", "value": summary.totalContacts.toString() },
      { "name": "Email Opens", "value": summary.emailOpens.toString() },
      { "name": "Email Clicks", "value": summary.emailClicks.toString() },
      { "name": "Active Deals", "value": summary.activeDeals.toString() },
      { "name": "Total Pipeline Value", "value": formatter.format(summary.totalDealValue) },
      { "name": "Closed Deals", "value": summary.closedDeals.toString() }
    ]
  }]
};

return [{ json: message }];

Why this works:
The Function node processes all items at once (Run Once for All Items), enabling cross-record aggregation. Using reduce() efficiently sums metrics across all contacts and deals. The MessageCard format creates visually structured Teams messages with clear metric labels.

Customization options:

  • Add topContacts array to highlight most engaged customers
  • Include deal stage breakdown to show pipeline health
  • Calculate week-over-week growth percentages for trending

Step 7: Post Summary to Microsoft Teams

The final step delivers your formatted summary to Teams using the webhook configured in Step 2.

Add HTTP Request node:

  1. Add "HTTP Request" node after the Function node
  2. Set Method: "POST"
  3. Set URL: Your Teams webhook URL from Step 2
  4. Set Body Content Type: "JSON"
  5. Set Body: {{$json}}
  6. Under Options → Response, set "Response Format": "JSON"

Node configuration:

{
  "method": "POST",
  "url": "https://outlook.office.com/webhook/YOUR-WEBHOOK-URL",
  "sendBody": true,
  "bodyContentType": "json",
  "body": "={{$json}}",
  "options": {
    "response": {
      "response": {
        "responseFormat": "json"
      }
    }
  }
}

Why this approach:
Teams webhooks expect MessageCard JSON format for rich formatting. Passing {{$json}} directly sends the formatted message from the Function node without additional transformation. The POST method with JSON content type matches Teams' API requirements.

Error handling:

  • HTTP 400 errors indicate malformed MessageCard JSON → Validate your Function node output
  • HTTP 404 errors mean webhook URL is invalid or deleted → Regenerate webhook in Teams
  • Timeouts suggest network issues → Add retry logic in n8n settings

Workflow Architecture Overview

This workflow consists of 6 nodes organized into 3 main sections:

  1. Scheduling (Node 1): Schedule Trigger initiates execution every Monday at 9 AM
  2. Data retrieval (Nodes 2-3): Two HubSpot nodes pull contacts and deals from the past 7 days
  3. Processing and delivery (Nodes 4-5): Function node aggregates metrics, HTTP Request posts to Teams

Execution flow:

  • Trigger: Weekly on Monday at 9:00 AM (configured timezone)
  • Average run time: 15-30 seconds depending on data volume
  • Key dependencies: HubSpot API access, Teams webhook URL, network connectivity

Critical nodes:

  • HubSpot (Contact): Retrieves customer engagement data using lastmodifieddate filter
  • Function: Transforms raw API data into aggregated metrics and MessageCard format
  • HTTP Request: Delivers formatted summary to Teams channel via webhook

The complete n8n workflow JSON template is available at the bottom of this article.

Testing & Validation

Before deploying to production, test each component individually to ensure data flows correctly.

Test HubSpot connection:

  1. Click "Execute Node" on the first HubSpot node
  2. Verify it returns contact records with expected properties
  3. Check that lastmodifieddate filter works by comparing returned dates
  4. If no results appear, adjust the date filter to 30 days for testing

Test data transformation:

  1. Execute the Function node after HubSpot nodes complete
  2. Inspect the output JSON structure
  3. Verify all metrics calculate correctly (check totalContacts, emailOpens, etc.)
  4. Confirm MessageCard format matches Teams requirements

Test Teams delivery:

  1. Execute the HTTP Request node
  2. Check your Teams channel for the posted message
  3. Verify formatting displays correctly with all metrics visible
  4. Confirm theme color and title appear as expected

Common issues:

  • Empty metrics → HubSpot filter too restrictive, expand date range for testing
  • Teams message not appearing → Webhook URL incorrect or expired
  • Calculation errors → Check for null values in HubSpot data, add null handling in Function node

Production Deployment Checklist

Area Requirement Why It Matters
Error Handling Add Error Trigger workflow to catch failures Prevents silent failures; alerts you to API issues within minutes
Monitoring Enable workflow execution history (keep last 100 runs) Troubleshoot issues by reviewing past executions and identifying patterns
Rate Limits Add 2-second wait between HubSpot API calls if retrieving >100 records HubSpot enforces 100 requests per 10 seconds; prevents 429 errors
Credentials Store API keys in n8n credential manager, never in node fields Protects sensitive data; enables credential rotation without workflow changes
Documentation Add sticky notes to each node explaining its purpose Reduces modification time by 2-3 hours when updating workflow months later
Backup Export workflow JSON weekly and store in version control Enables rollback if changes break functionality

Real-World Use Cases

Use Case 1: SaaS Customer Success Teams

  • Industry: B2B SaaS
  • Scale: 200-500 active customers
  • Modifications needed: Add product usage metrics from your application database using HTTP Request node to pull from internal API; include churn risk scores calculated from engagement patterns

Use Case 2: Agency Client Reporting

  • Industry: Marketing agency
  • Scale: 20-50 clients
  • Modifications needed: Filter contacts by company (client); create separate Teams channels per client; modify Function node to generate client-specific summaries with campaign performance metrics

Use Case 3: Sales Team Pipeline Reviews

  • Industry: Enterprise B2B sales
  • Scale: 50-100 active opportunities
  • Modifications needed: Focus on deal progression rather than contact engagement; add deal stage velocity calculations; include sales rep attribution for accountability

Customizations & Extensions

Alternative Integrations

Instead of Microsoft Teams:

  • Slack: Use Slack webhook node (same MessageCard format works with Block Kit conversion) - requires 2 node changes
  • Email: Replace HTTP Request with Email Send node for distribution to stakeholders without Teams access
  • Google Chat: Swap Teams webhook for Google Chat webhook - requires reformatting message structure in Function node

Workflow Extensions

Add automated follow-up tasks:

  • Add HubSpot node after summary generation
  • Create tasks for sales reps based on high-engagement contacts
  • Assign tasks automatically using contact owner property
  • Nodes needed: +3 (Function to identify high-engagement contacts, HubSpot Create Task, Set)

Scale to handle more data:

  • Replace single HubSpot call with batched requests (100 contacts per batch)
  • Add Loop node to process batches sequentially
  • Implement pagination handling for >10,000 records
  • Performance improvement: Handles unlimited contacts without timeout

Integration possibilities:

Add This To Get This Complexity
Google Sheets export Historical tracking and trend analysis Easy (3 nodes: Function, Google Sheets, Set)
Sentiment analysis AI-powered engagement quality scoring Medium (5 nodes: HTTP Request to OpenAI, Function, If)
Salesforce sync Cross-CRM reporting for hybrid teams Medium (6 nodes: Salesforce Get, Merge, Function)
Power BI connector Executive dashboards with visualizations Hard (10+ nodes: HTTP Request to Power BI API, authentication)

Get Started Today

Ready to automate your customer engagement reporting?

  1. Download the template: Scroll to the bottom of this article to copy the complete n8n workflow JSON
  2. Import to n8n: Go to Workflows → Import from File, paste the JSON
  3. Configure your services: Add your HubSpot API credentials and Teams webhook URL
  4. Test with sample data: Execute each node individually to verify connections work
  5. Deploy to production: Activate the workflow and wait for your first Monday morning summary

This workflow eliminates 3-5 hours of manual reporting every week while ensuring your team never misses critical customer engagement patterns.

Need help customizing this workflow for your specific CRM setup or extending it into lead nurturing automation? Schedule an intro call with Atherial.


Complete n8n Workflow JSON Template

{
  "name": "Weekly HubSpot Customer Engagement Summary to Teams",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtHour": 9,
              "triggerAtMinute": 0,
              "triggerOnWeekday": 1
            }
          ]
        }
      },
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [250, 300]
    },
    {
      "parameters": {
        "resource": "contact",
        "operation": "getAll",
        "returnAll": true,
        "filters": {
          "filterGroups": [
            {
              "filters": [
                {
                  "propertyName": "lastmodifieddate",
                  "operator": "GT",
                  "value": "={{$now.minus({days: 7}).toISO()}}"
                }
              ]
            }
          ]
        },
        "additionalFields": {
          "properties": [
            "email",
            "firstname",
            "lastname",
            "hs_email_open",
            "hs_email_click",
            "notes_last_updated",
            "num_associated_deals"
          ]
        }
      },
      "name": "HubSpot Get Contacts",
      "type": "n8n-nodes-base.hubspot",
      "typeVersion": 1,
      "position": [450, 300],
      "credentials": {
        "hubspotApi": {
          "id": "1",
          "name": "HubSpot API"
        }
      }
    },
    {
      "parameters": {
        "resource": "deal",
        "operation": "getAll",
        "returnAll": true,
        "filters": {
          "filterGroups": [
            {
              "filters": [
                {
                  "propertyName": "hs_lastmodifieddate",
                  "operator": "GT",
                  "value": "={{$now.minus({days: 7}).toISO()}}"
                }
              ]
            }
          ]
        },
        "additionalFields": {
          "properties": [
            "dealname",
            "amount",
            "dealstage",
            "closedate",
            "pipeline",
            "hs_lastmodifieddate"
          ]
        }
      },
      "name": "HubSpot Get Deals",
      "type": "n8n-nodes-base.hubspot",
      "typeVersion": 1,
      "position": [450, 500],
      "credentials": {
        "hubspotApi": {
          "id": "1",
          "name": "HubSpot API"
        }
      }
    },
    {
      "parameters": {
        "functionCode": "const contacts = $input.first().json;
const deals = $input.last().json;

const summary = {
  totalContacts: contacts.length,
  emailOpens: contacts.reduce((sum, c) => sum + (c.hs_email_open || 0), 0),
  emailClicks: contacts.reduce((sum, c) => sum + (c.hs_email_click || 0), 0),
  activeDeals: deals.length,
  totalDealValue: deals.reduce((sum, d) => sum + (parseFloat(d.amount) || 0), 0),
  closedDeals: deals.filter(d => d.dealstage === 'closedwon').length
};

const formatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  minimumFractionDigits: 0
});

const message = {
  \"@type\": \"MessageCard\",
  \"@context\": \"https://schema.org/extensions\",
  \"summary\": \"Weekly Customer Engagement Summary\",
  \"themeColor\": \"0078D4\",
  \"title\": \"📊 Weekly Customer Engagement Summary\",
  \"sections\": [{
    \"activityTitle\": `Week of ${$now.minus({days: 7}).toFormat('MMM dd')} - ${$now.toFormat('MMM dd, yyyy')}`,
    \"facts\": [
      { \"name\": \"Active Contacts\", \"value\": summary.totalContacts.toString() },
      { \"name\": \"Email Opens\", \"value\": summary.emailOpens.toString() },
      { \"name\": \"Email Clicks\", \"value\": summary.emailClicks.toString() },
      { \"name\": \"Active Deals\", \"value\": summary.activeDeals.toString() },
      { \"name\": \"Total Pipeline Value\", \"value\": formatter.format(summary.totalDealValue) },
      { \"name\": \"Closed Deals\", \"value\": summary.closedDeals.toString() }
    ]
  }]
};

return [{ json: message }];"
      },
      "name": "Transform to Teams Message",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [650, 400]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "YOUR_TEAMS_WEBHOOK_URL_HERE",
        "sendBody": true,
        "bodyContentType": "json",
        "body": "={{$json}}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "name": "Post to Teams",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [850, 400]
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "HubSpot Get Contacts",
            "type": "main",
            "index": 0
          },
          {
            "node": "HubSpot Get Deals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HubSpot Get Contacts": {
      "main": [
        [
          {
            "node": "Transform to Teams Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HubSpot Get Deals": {
      "main": [
        [
          {
            "node": "Transform to Teams Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transform to Teams Message": {
      "main": [
        [
          {
            "node": "Post to Teams",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Complete N8N Workflow Template

Copy the JSON below and import it into your N8N instance via Workflows → Import from File

{
  "name": "HubSpot to Teams Weekly Summary",
  "nodes": [
    {
      "id": "weekly-trigger",
      "name": "Weekly Schedule Trigger",
      "type": "n8n-nodes-base.cron",
      "position": [
        250,
        300
      ],
      "parameters": {
        "triggerTimes": {
          "item": [
            {
              "hour": 9,
              "mode": "everyWeek",
              "minute": 0,
              "dayOfWeek": "monday"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "get-contacts",
      "name": "Get HubSpot Contacts",
      "type": "n8n-nodes-base.hubspot",
      "position": [
        500,
        200
      ],
      "parameters": {
        "limit": 100,
        "resource": "contact",
        "operation": "getMany",
        "authentication": "apiKey",
        "additionalFields": {
          "properties": [
            "firstname",
            "lastname",
            "email",
            "phone",
            "lifecyclestage",
            "hs_lead_status",
            "lastmodifieddate"
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "get-deals",
      "name": "Get HubSpot Deals",
      "type": "n8n-nodes-base.hubspot",
      "position": [
        500,
        400
      ],
      "parameters": {
        "limit": 100,
        "resource": "deal",
        "operation": "getMany",
        "authentication": "apiKey",
        "additionalFields": {
          "properties": [
            "dealname",
            "dealstage",
            "dealowner",
            "amount",
            "closedate",
            "hs_lastmodifieddate"
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "get-activities",
      "name": "Get HubSpot Activities",
      "type": "n8n-nodes-base.hubspot",
      "position": [
        500,
        600
      ],
      "parameters": {
        "limit": 100,
        "resource": "engagement",
        "operation": "getMany",
        "authentication": "apiKey",
        "additionalFields": {
          "engagement_type": "all"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "aggregate-data",
      "name": "Aggregate & Format Data",
      "type": "n8n-nodes-base.code",
      "position": [
        850,
        400
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Aggregate data from HubSpot\nconst contacts = $('Get HubSpot Contacts').all()[0]?.json?.body || [];\nconst deals = $('Get HubSpot Deals').all()[0]?.json?.body || [];\nconst activities = $('Get HubSpot Activities').all()[0]?.json?.body || [];\n\n// Calculate weekly statistics\nconst totalContacts = contacts.length || 0;\nconst newContacts = contacts.filter(c => {\n  const modified = new Date(c.properties?.lastmodifieddate?.value || 0);\n  const weekAgo = new Date();\n  weekAgo.setDate(weekAgo.getDate() - 7);\n  return modified >= weekAgo;\n}).length || 0;\n\nconst totalDeals = deals.length || 0;\nconst dealsWon = deals.filter(d => d.properties?.dealstage?.value === 'closedwon').length || 0;\nconst dealsLost = deals.filter(d => d.properties?.dealstage?.value === 'closedlost').length || 0;\nconst dealValue = deals.reduce((sum, d) => {\n  const amount = parseFloat(d.properties?.amount?.value || 0);\n  return sum + amount;\n}, 0);\n\nconst totalActivities = activities.length || 0;\nconst emailsLogged = activities.filter(a => a.engagement?.type === 'EMAIL').length || 0;\nconst callsLogged = activities.filter(a => a.engagement?.type === 'CALL').length || 0;\nconst meetingsLogged = activities.filter(a => a.engagement?.type === 'MEETING').length || 0;\n\n// Stage distribution\nconst stageDistribution = deals.reduce((acc, d) => {\n  const stage = d.properties?.dealstage?.value || 'Unknown';\n  acc[stage] = (acc[stage] || 0) + 1;\n  return acc;\n}, {});\n\n// Top contacts by recent activity\nconst recentContacts = contacts\n  .sort((a, b) => {\n    const dateA = new Date(a.properties?.lastmodifieddate?.value || 0);\n    const dateB = new Date(b.properties?.lastmodifieddate?.value || 0);\n    return dateB - dateA;\n  })\n  .slice(0, 5)\n  .map(c => ({\n    name: `${c.properties?.firstname?.value || ''} ${c.properties?.lastname?.value || ''}`.trim() || 'Unknown',\n    email: c.properties?.email?.value || 'N/A',\n    stage: c.properties?.lifecyclestage?.value || 'N/A'\n  }));\n\n// Top deals\nconst topDeals = deals\n  .filter(d => d.properties?.amount?.value)\n  .sort((a, b) => {\n    const amountA = parseFloat(a.properties?.amount?.value || 0);\n    const amountB = parseFloat(b.properties?.amount?.value || 0);\n    return amountB - amountA;\n  })\n  .slice(0, 5)\n  .map(d => ({\n    name: d.properties?.dealname?.value || 'Unnamed Deal',\n    stage: d.properties?.dealstage?.value || 'Unknown',\n    amount: `$${parseFloat(d.properties?.amount?.value || 0).toLocaleString('en-US', { minimumFractionDigits: 2 })}`,\n    closeDate: d.properties?.closedate?.value || 'N/A'\n  }));\n\nreturn [{\n  json: {\n    summary: {\n      totalContacts,\n      newContacts,\n      totalDeals,\n      dealsWon,\n      dealsLost,\n      dealValue: `$${dealValue.toLocaleString('en-US', { minimumFractionDigits: 2 })}`,\n      totalActivities,\n      emailsLogged,\n      callsLogged,\n      meetingsLogged\n    },\n    stageDistribution,\n    recentContacts,\n    topDeals,\n    generatedAt: new Date().toISOString()\n  }\n}];",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "format-message",
      "name": "Format Teams Message",
      "type": "n8n-nodes-base.code",
      "position": [
        1150,
        400
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const data = $input.json;\nconst summary = data.summary;\nconst stageDistribution = data.stageDistribution;\nconst recentContacts = data.recentContacts;\nconst topDeals = data.topDeals;\nconst currentDate = new Date();\nconst weekStart = new Date(currentDate);\nweekStart.setDate(weekStart.getDate() - weekStart.getDay());\nconst dateRange = `${weekStart.toLocaleDateString()} - ${currentDate.toLocaleDateString()}`;\n\n// Build contact facts\nconst contactFacts = [\n  { name: 'Total Contacts', value: summary.totalContacts.toString() },\n  { name: 'New Contacts', value: summary.newContacts.toString() }\n];\n\n// Build deal facts\nconst dealFacts = [\n  { name: 'Total Deals', value: summary.totalDeals.toString() },\n  { name: 'Won This Week', value: summary.dealsWon.toString() },\n  { name: 'Lost This Week', value: summary.dealsLost.toString() },\n  { name: 'Pipeline Value', value: summary.dealValue }\n];\n\n// Build activity facts\nconst activityFacts = [\n  { name: 'Total Activities Logged', value: summary.totalActivities.toString() },\n  { name: 'Emails', value: summary.emailsLogged.toString() },\n  { name: 'Calls', value: summary.callsLogged.toString() },\n  { name: 'Meetings', value: summary.meetingsLogged.toString() }\n];\n\n// Build stage distribution section\nconst stageText = Object.entries(stageDistribution)\n  .map(([stage, count]) => `- **${stage}**: ${count} deals`)\n  .join('\\\\n');\n\n// Build recent contacts section\nconst contactsText = recentContacts\n  .map(c => `- **${c.name}** (${c.email}) - Stage: ${c.stage}`)\n  .join('\\\\n');\n\n// Build top deals section\nconst dealsText = topDeals\n  .map(d => `- **${d.name}** - ${d.stage} - ${d.amount}`)\n  .join('\\\\n');\n\nreturn [{\n  json: {\n    messageBody: {\n      contentType: 'html',\n      content: `\n<h2>📊 HubSpot Weekly Summary Report</h2>\n<p><strong>Week of ${dateRange}</strong></p>\n\n<h3>👥 Contact Metrics</h3>\n<ul>\n  <li><strong>Total Contacts:</strong> ${summary.totalContacts}</li>\n  <li><strong>New Contacts This Week:</strong> ${summary.newContacts}</li>\n</ul>\n\n<h3>💼 Deal Pipeline</h3>\n<ul>\n  <li><strong>Total Deals:</strong> ${summary.totalDeals}</li>\n  <li><strong>Deals Won:</strong> ${summary.dealsWon}</li>\n  <li><strong>Deals Lost:</strong> ${summary.dealsLost}</li>\n  <li><strong>Pipeline Value:</strong> ${summary.dealValue}</li>\n</ul>\n\n<h3>🎯 Deal Stage Distribution</h3>\n<pre>${stageText}</pre>\n\n<h3>📞 Engagement Activities</h3>\n<ul>\n  <li><strong>Total Activities Logged:</strong> ${summary.totalActivities}</li>\n  <li><strong>Emails:</strong> ${summary.emailsLogged}</li>\n  <li><strong>Calls:</strong> ${summary.callsLogged}</li>\n  <li><strong>Meetings:</strong> ${summary.meetingsLogged}</li>\n</ul>\n\n<h3>🔥 Top 5 Active Contacts</h3>\n<pre>${contactsText || 'No recent activity'}</pre>\n\n<h3>💰 Top 5 Deals by Value</h3>\n<pre>${dealsText || 'No deals'}</pre>\n\n<hr/>\n<p><em>Report generated: ${new Date().toLocaleString()}</em></p>\n      `\n    },\n    contactFacts,\n    dealFacts,\n    activityFacts\n  }\n}];",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "send-teams-message",
      "name": "Post Summary to Teams",
      "type": "n8n-nodes-base.microsoftTeams",
      "position": [
        1450,
        400
      ],
      "parameters": {
        "teamId": "={{$env.TEAMS_TEAM_ID}}",
        "resource": "channelMessage",
        "channelId": "={{$env.TEAMS_CHANNEL_ID}}",
        "operation": "create",
        "jsonMessage": "{\n  \"@type\": \"MessageCard\",\n  \"@context\": \"https://schema.org/extensions\",\n  \"summary\": \"HubSpot Weekly Summary Report\",\n  \"themeColor\": \"0078D4\",\n  \"title\": \"📊 HubSpot Weekly Customer Engagement Summary\",\n  \"sections\": [\n    {\n      \"activityTitle\": \"Weekly Report\",\n      \"facts\": [\n        {\n          \"name\": \"Report Period\",\n          \"value\": \"{{ $now.format('MMM dd, yyyy') }} - Last 7 days\"\n        },\n        {\n          \"name\": \"Total Contacts\",\n          \"value\": \"{{ $input.json.summary.totalContacts }}\"\n        },\n        {\n          \"name\": \"New Contacts\",\n          \"value\": \"{{ $input.json.summary.newContacts }}\"\n        },\n        {\n          \"name\": \"Pipeline Value\",\n          \"value\": \"{{ $input.json.summary.dealValue }}\"\n        },\n        {\n          \"name\": \"Deals Won\",\n          \"value\": \"{{ $input.json.summary.dealsWon }}\"\n        },\n        {\n          \"name\": \"Total Activities\",\n          \"value\": \"{{ $input.json.summary.totalActivities }}\"\n        }\n      ]\n    }\n  ]\n}",
        "messageInput": "json"
      },
      "typeVersion": 2
    }
  ],
  "connections": {
    "Get HubSpot Deals": {
      "main": [
        [
          {
            "node": "Aggregate & Format Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Teams Message": {
      "main": [
        [
          {
            "node": "Post Summary to Teams",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get HubSpot Contacts": {
      "main": [
        [
          {
            "node": "Aggregate & Format Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get HubSpot Activities": {
      "main": [
        [
          {
            "node": "Aggregate & Format Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate & Format Data": {
      "main": [
        [
          {
            "node": "Format Teams Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Weekly Schedule Trigger": {
      "main": [
        [
          {
            "node": "Get HubSpot Contacts",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get HubSpot Deals",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get HubSpot Activities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}