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 flow
Microsoft Outlook logo
Source
Microsoft Outlook
HubSpot logo
Process
HubSpot
n8n logo
Process
n8n
Microsoft Teams logo
Destination
Microsoft Teams

The stack in the order it runs — data flows from the source through to where it lands.

Why this stack

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)

  1. Microsoft Outlook logo

    Mail + calendar at the center of most companies.

    Graph API turns the inbox/calendar into a programmable source for ops automations.

  2. HubSpot logo

    CRM + marketing for go-to-market.

    Pipeline data becomes an automatable input for revenue reports.

  3. n8n logo

    Self-hostable workflow automation.

    Own your data and run unlimited steps without per-task pricing.

  4. Microsoft Teams logo

    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. 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. 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. 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. 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. 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. 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. 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 services

More like this