Auto-archive stale HubSpot deals using n8n

high complexityCost: $0-24/moRecommended

Prerequisites

Prerequisites
  • n8n instance (cloud or self-hosted)
  • HubSpot private app token with crm.objects.deals.read, crm.objects.deals.write, and crm.schemas.deals.read scopes
  • Slack app with Bot Token (chat:write scope)
  • n8n credentials for HubSpot and Slack
  • A "Closed Lost — Stale" close reason configured in HubSpot (optional but recommended)

Overview

This workflow runs daily, finds deals with no activity for 60+ days, warns the deal owner in Slack, then waits 48 hours. If the deal still hasn't been updated after the grace period, it moves the deal to Closed Lost automatically.

n8n's Wait node makes this a natural fit — the workflow literally pauses for 48 hours mid-execution without consuming resources.

Step 1: Schedule a daily trigger

Add a Schedule Trigger node:

  • Trigger interval: Days
  • Trigger at hour: 7 (run before the team starts)
  • Timezone: Set to your team's timezone

Step 2: Search for stale deals

Add an HTTP Request node to find deals with no modification in the last 60 days:

  • Method: POST
  • URL: https://api.hubapi.com/crm/v3/objects/deals/search
  • Authentication: HubSpot API credential
  • Body:
{
  "filterGroups": [
    {
      "filters": [
        {
          "propertyName": "hs_lastmodifieddate",
          "operator": "LT",
          "value": "{{ $now.minus({days: 60}).toMillis() }}"
        },
        {
          "propertyName": "dealstage",
          "operator": "NOT_IN",
          "values": ["closedwon", "closedlost"]
        }
      ]
    }
  ],
  "properties": [
    "dealname", "amount", "dealstage", "hubspot_owner_id",
    "hs_lastmodifieddate"
  ],
  "sorts": [
    { "propertyName": "hs_lastmodifieddate", "direction": "ASCENDING" }
  ],
  "limit": 100
}
Timestamp format

HubSpot's LT operator for dates expects a Unix timestamp in milliseconds. n8n's $now.minus({days: 60}).toMillis() handles this correctly.

Step 3: Check if any deals were found

Add an IF node after the search:

  • Condition: {{ $json.total }} greater than 0

Connect the true branch to the next step. The false branch ends the workflow — nothing to do today.

Step 4: Split into individual deals

Add a Split Out node to iterate over each stale deal:

  • Field to split out: results

This produces one item per deal, so each deal flows through the remaining nodes individually.

Step 5: Resolve the deal owner

Add an HTTP Request node to look up the owner's name and email:

  • Method: GET
  • URL: https://api.hubapi.com/crm/v3/owners/{{ $json.properties.hubspot_owner_id }}
  • Authentication: HubSpot API credential

This returns firstName, lastName, and email for the deal owner.

Unassigned deals

If hubspot_owner_id is empty, this request will fail. Add an IF node before the owner lookup to check for an owner, and route unassigned deals to a separate Slack channel or skip them.

Step 6: Warn the deal owner in Slack

Add a Slack node to notify the owner:

  • Resource: Message
  • Operation: Send a Message
  • Channel: #sales-pipeline (or DM the owner if you have a HubSpot-to-Slack user mapping)
  • Message Type: Block Kit
{
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "⚠️ *Stale Deal Warning*\n\n*{{ $('Split Out').item.json.properties.dealname }}* has had no activity for {{ Math.round((Date.now() - new Date($('Split Out').item.json.properties.hs_lastmodifieddate)) / 86400000) }} days.\n\nThis deal will be moved to *Closed Lost* in 48 hours unless you update it.\n\n<https://app.hubspot.com/contacts/YOUR_PORTAL_ID/deal/{{ $('Split Out').item.json.id }}|View deal in HubSpot>"
      }
    }
  ]
}

Replace YOUR_PORTAL_ID with your actual HubSpot portal ID.

Step 7: Wait 48 hours

Add a Wait node:

  • Resume: After time interval
  • Wait amount: 48
  • Wait unit: Hours

The workflow pauses here. No executions are consumed while waiting.

How the Wait node works

n8n persists the workflow state and resumes after 48 hours. On n8n cloud, this is fully managed. Self-hosted instances need a running n8n process — if n8n restarts, queued waits resume automatically from the database.

Step 8: Re-check if the deal was updated

After the wait, the deal may have been updated by the rep. Add an HTTP Request node to fetch the deal's current state:

  • Method: GET
  • URL: https://api.hubapi.com/crm/v3/objects/deals/{{ $('Split Out').item.json.id }}
  • Query params: properties=hs_lastmodifieddate,dealstage

Then add an IF node to check:

  • Condition: {{ new Date($json.properties.hs_lastmodifieddate) > $now.minus({hours: 48}).toJSDate() }}

If true (deal was updated during the grace period), the workflow ends for this deal — the rep saved it.

If false (still stale), continue to close it.

Step 9: Close the deal

Add an HTTP Request node on the false branch:

  • Method: PATCH
  • URL: https://api.hubapi.com/crm/v3/objects/deals/{{ $('Split Out').item.json.id }}
  • Body:
{
  "properties": {
    "dealstage": "closedlost",
    "closed_lost_reason": "Stale — auto-archived after 60 days"
  }
}
Custom close reason property

The closed_lost_reason property name depends on your HubSpot configuration. Check your deal properties in HubSpot Settings to find the internal name. Some portals use hs_closed_lost_reason or a custom property.

Step 10: Send a confirmation to Slack

Add a final Slack node:

{
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "🗄️ *Deal Auto-Archived*\n*{{ $('Split Out').item.json.properties.dealname }}* was moved to Closed Lost after 60+ days of inactivity. No objection was received during the 48-hour grace period."
      }
    }
  ]
}

Step 11: Add error handling and activate

  1. Enable Retry On Fail on all HTTP Request nodes (2 retries, 5 second wait)
  2. Create an Error Workflow that notifies you in Slack if the main workflow fails
  3. Test manually by clicking Execute Workflow (use a test deal to verify the full flow)
  4. Toggle the workflow to Active

Cost

  • n8n Cloud: Each daily run uses 1 execution for the trigger, plus additional node executions per stale deal. The Wait node does not consume credits while paused. A typical run with 5 stale deals uses ~50 node executions. Well within the Starter plan (2,500 executions/month).
  • Self-hosted: Free.

Need help implementing this?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.