Outlook + HubSpot Contact Inactivity: Weekly Re-Engagement Risk Digest to Microsoft Teams via n8n
Every Friday at 09:00, surface every HubSpot contact in an active deal stage who hasn't had an Outlook email touch in 14+ days — before the deal quietly dies.
The stack in the order it runs — data flows from the source through to where it lands.
Deal slippage from inactivity is the most preventable revenue loss in early-stage B2B SaaS. The pattern is always the same: contact goes quiet, rep assumes they're thinking it over, two weeks pass, deal dies. Most CRMs have a 'last contacted' field — but it only updates when the rep logs the activity manually. They don't. About 60% of the time, that field is stale.
Cross-referencing Outlook sent mail against HubSpot deal stage gives you ground truth. Not whatever the rep last remembered to click. The Microsoft Graph API powers Outlook data access, and HubSpot's CRM API handles deal and contact lookups. n8n can call both in sequence, with looping logic to handle pagination and per-contact email history checks. That's 20–30 lines of JavaScript in Code nodes. You can do this in Make, but the nested loops for per-contact Graph lookups get messy fast.
Teams is the right delivery channel because this is a weekly ops review digest, not an emergency alert. Teams' threaded message format lets the sales manager reply directly in the thread to assign follow-ups. Slack's channel model is less clean for that in a sales context.
The workflow does not send emails or update CRM records. That's intentional. Automating CRM updates based on inferred contact patterns creates data integrity nightmares. A human reviews and acts. This workflow just makes sure they see the right information at the right time. One hard limit: if your HubSpot has more than 2,000 active pipeline contacts, the per-contact Graph API lookup will hit rate limits. Add a 500ms delay between calls or switch to the Graph batch endpoint. If your team uses Gmail instead of Outlook, swap the Microsoft Graph steps for Gmail API calls via OAuth — the rest of the workflow is identical.
The stack (4)
Mail + calendar at the center of most companies.
Graph API turns the inbox/calendar into a programmable source for ops automations.
Chat + channels for Microsoft-365 shops.
If the company is on Outlook/365, Teams is where reports get read — push there, not a separate tool.
How it runs
- 1
Authenticate n8n with Microsoft Graph and HubSpot
In n8n, create credentials for Microsoft Graph API using OAuth 2.0. Go to Azure Portal, register an app, grant `Mail.Read` and `User.Read.All` delegated permissions, then copy the client ID and secret into n8n's Microsoft OAuth2 credential. For HubSpot, create a Private App under HubSpot Settings → Integrations → Private Apps with scopes `crm.objects.contacts.read` and `crm.objects.deals.read`. Store the token in n8n HubSpot credentials. Test both connections before you build anything else — a broken auth discovered mid-workflow wastes 30 minutes.
- 2
Fetch active pipeline deals from HubSpot
Add an HTTP Request node pointed at the HubSpot API. Method: POST. URL: `https://api.hubapi.com/crm/v3/objects/deals/search`. Body: `{ filterGroups: [{filters: [{propertyName: 'dealstage', operator: 'NOT_IN', values: ['closedwon','closedlost']}]}], properties: ['dealname','dealstage','hubspot_owner_id','associations'], limit: 100 }`. Paginate using `paging.next.after` with a Loop node if `total > 100`. This is your working set — closed deals are noise, skip them entirely.
- 3
Resolve associated contacts for each deal
Add a Split In Batches node to process deals one at a time. For each deal, add an HTTP Request node calling `https://api.hubapi.com/crm/v3/objects/deals/{{dealId}}/associations/contacts`. Extract the first associated contact ID. Then call `https://api.hubapi.com/crm/v3/objects/contacts/{{contactId}}?properties=email,firstname,lastname,hs_sales_email_last_replied` to get the contact's email address and HubSpot's built-in last-replied timestamp. You'll use that timestamp as a cross-check in step 6.
- 4
Check Outlook sent mail for each contact's email
For each contact email, add an HTTP Request node calling the Microsoft Graph API: GET `https://graph.microsoft.com/v1.0/me/mailFolders/SentItems/messages?$filter=toRecipients/any(r:r/emailAddress/address eq '{{contactEmail}}')&$orderby=sentDateTime desc&$top=1&$select=sentDateTime,subject`. This returns the most recent email sent to that contact. Extract `sentDateTime`. If the API returns an empty array, the contact has never been emailed — treat this as 9999 days since last contact.
- 5
Compute days since last touch and filter by threshold
Add a Code node. Compute `daysSinceLastEmail = Math.floor((Date.now() - new Date(sentDateTime).getTime()) / 86400000)`. Threshold is 14 days — if your sales cycle is enterprise, drop it to 7. Filter to only pass through contacts where `daysSinceLastEmail >= 14`. Add a secondary flag `neverEmailed = true` if the Outlook query returned no results. Output an array of objects: `{ dealName, contactName, contactEmail, dealStage, ownerName, daysSinceLastEmail, lastSubject }`.
- 6
Aggregate results and format the Teams message
Add an Aggregate node to collect all flagged contacts into a single array. Add a Code node to sort by `daysSinceLastEmail` descending, then build the Teams message payload. Structure: one intro line — `N deals at risk of going cold — week of [date]` — then a table-style list per deal: `• [ContactName] / [DealName] ([Stage]) — last touched [X] days ago by [Owner] — subject: "[lastSubject]"`. One edge case worth flagging: if `hs_sales_email_last_replied` is within 7 days despite no sent mail showing up, note it — the contact may have replied to an email sent from a different account.
- 7
Post to Microsoft Teams and schedule the workflow
Add a Microsoft Teams node or an HTTP Request to your Teams incoming webhook URL. Post to your `#sales-ops` channel or a dedicated 'Deal Health' channel. If the filtered array is empty, post a green-flag summary: `✅ All active deals touched within 14 days — nothing to flag this week.` Schedule with n8n's Schedule Trigger node: every Friday at 09:00. Wrap the entire workflow in a Try/Catch that posts a failure notice to Teams if any step errors. Microsoft Graph token expiry is the most common failure mode here — and silent failures in sales ops are how deals die without anyone noticing.
Want me to build this for you instead?
Product Audit and CTO Mode run out of this same thinking. If you’re reading this thinking “I want this, but in my product” — let’s talk.
See servicesMore like this
Gmail Thread Aging + Stripe Invoice Overdue: Unified AR Follow-Up Digest to Slack via Zapier
Surface overdue Stripe invoices and the exact age of your last Gmail thread with that customer — every morning at 8:30, automatically — so AR follow-up stops living in someone's head.
Intercom Ticket Volume + Razorpay Failed Payments: Daily Support-Cost-per-Revenue Alert to Slack via Zapier
Catch the support cost blowout from Razorpay failed payments before your agents are already buried.
Outlook Calendar Load + HubSpot Deal Velocity: Weekly Ops Digest to Microsoft Teams
Every Monday at 07:30, your revenue team gets one Teams card: last week's deal pipeline movement next to each rep's actual meeting load — so you stop guessing whether low close rates are a pipeline problem or a capacity problem.