AutomationFlowsAI & RAG › Enforce Sales Rules of Engagement with Agentmail for Hubspot Teams

Enforce Sales Rules of Engagement with Agentmail for Hubspot Teams

ByStephan Koning @reklaim on n8n.io

This is for SaaS founders, agency owners, and Sales Ops managers who use HubSpot but are tired of "toe-stepping." If your BDRs are accidentally emailing your AE’s active deals, or Marketing is blasting "Closed Won" accounts with generic newsletters, your CRM has failed you. This…

Event trigger★★★★★ complexity65 nodesData TableHTTP Request
AI & RAG Trigger: Event Nodes: 65 Complexity: ★★★★★ Added:

This workflow corresponds to n8n.io template #15122 — we link there as the canonical source.

This workflow follows the Datatable → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "9f5dac0c-0427-4d4e-a49d-0c55898574e2",
      "name": "Note: Database Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        480,
        -7312
      ],
      "parameters": {
        "color": "#97A3D3",
        "width": 1088,
        "height": 468,
        "content": "## \ud83d\uddc4\ufe0f DATABASE SETUP: Initialize `Team_Config`                                        \n\n**Run this ONCE** \n\n1. It creates an n8n Data Table called `Team_Config`.\n2. It defines the exact columns needed for AgentMail hierarchy enforcement.\n3. It injects the default 3-tier system (AE, BDR, Marketing).\n\nAfter running this, go to **Data (left menu) -> Team_Config** in n8n to view and edit your team's email addresses like a spreadsheet."
      },
      "typeVersion": 1
    },
    {
      "id": "55e35b96-dfa8-402a-805a-89858f84e7e7",
      "name": "1. Create Schema",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        624,
        -7008
      ],
      "parameters": {
        "columns": {
          "column": [
            {
              "name": "tier_id"
            },
            {
              "name": "display_name"
            },
            {
              "name": "inbox_address"
            },
            {
              "name": "client_id"
            },
            {
              "name": "priority",
              "type": "number"
            },
            {
              "name": "escalates_to"
            },
            {
              "name": "decay_days",
              "type": "number"
            }
          ]
        },
        "options": {
          "createIfNotExists": true
        },
        "resource": "table",
        "operation": "create",
        "tableName": "Team_Config"
      },
      "typeVersion": 1.1
    },
    {
      "id": "5f1bb5f5-4af9-4a0a-a6c2-b0e72a4f8657",
      "name": "2. Generate Baseline Tiers",
      "type": "n8n-nodes-base.code",
      "position": [
        832,
        -7008
      ],
      "parameters": {
        "jsCode": "return [\n  {\n    json: {\n      tier_id: 'ae',\n      display_name: 'Account Executive',\n      inbox_address: 'user@example.com',\n      client_id: 'tier_ae',\n      priority: 3,\n      escalates_to: ''\n    }\n  },\n  {\n    json: {\n      tier_id: 'bdr',\n      display_name: 'BDR / SDR',\n      inbox_address: 'user@example.com',\n      client_id: 'tier_bdr',\n      priority: 2,\n      escalates_to: 'ae'\n    }\n  },\n  {\n    json: {\n      tier_id: 'marketing',\n      display_name: 'Marketing',\n      inbox_address: 'user@example.com',\n      client_id: 'tier_mkt',\n      priority: 1,\n      escalates_to: 'bdr'\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "8648dbfe-5781-4900-99db-7573b4bbeb39",
      "name": "3. Populate Database",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        1056,
        -7008
      ],
      "parameters": {
        "columns": {
          "value": {},
          "column": [
            {
              "name": "={{ $json.tier_id }}"
            },
            {
              "name": "={{ $json.display_name }}"
            }
          ],
          "schema": [
            {
              "id": "tier_id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "tier_id",
              "defaultMatch": false
            },
            {
              "id": "display_name",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "display_name",
              "defaultMatch": false
            },
            {
              "id": "inbox_address",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "inbox_address",
              "defaultMatch": false
            },
            {
              "id": "client_id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "client_id",
              "defaultMatch": false
            },
            {
              "id": "priority",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "priority",
              "defaultMatch": false
            },
            {
              "id": "escalates_to",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "escalates_to",
              "defaultMatch": false
            },
            {
              "id": "decay_days",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "decay_days",
              "defaultMatch": false
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "optimizeBulk": false
        },
        "dataTableId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('1. Create Schema').item.json.id }}"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "1b338914-e5f8-4d96-8b4e-ca446dfe0667",
      "name": "GET Block Lists",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1056,
        -5520
      ],
      "parameters": {
        "url": "={{ 'https://api.agentmail.to/v0/inboxes/' + $json.inbox_address + '/lists/send/block' }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "a83b274a-86a9-4e11-a801-d6af3557ec9f",
      "name": "\ud83d\udcca Compile Audit Report",
      "type": "n8n-nodes-base.code",
      "position": [
        1280,
        -5520
      ],
      "parameters": {
        "jsCode": "const responses = $input.all();\nconst configRows = $('\u2699\ufe0f Team_Config').all();\nconst report = [];\nlet totalBlocked = 0;\n\nfor (let i = 0; i < responses.length; i++) {\n  const meta = configRows[i] ? configRows[i].json : {};\n  const resp = responses[i].json;\n  const entries = Array.isArray(resp.entries) ? resp.entries : Array.isArray(resp) ? resp : [];\n\n  report.push({\n    inbox_id: meta.inbox_address,\n    tier_id: meta.tier_id,\n    display_name: meta.display_name,\n    priority: meta.priority,\n    total_blocked: entries.length,\n    blocked_prospects: entries.map(e => ({\n      prospect: e.entry || 'unknown',\n      reason: e.reason || 'No reason',\n      blocked_at: e.created_at || 'unknown'\n    }))\n  });\n  totalBlocked += entries.length;\n}\n\nreport.sort((a, b) => (b.priority || 0) - (a.priority || 0));\n\nreturn [{ json: {\n  status: 'AUDIT_COMPLETE',\n  total_inboxes: report.length,\n  total_active_blocks: totalBlocked,\n  report,\n  generated_at: new Date().toISOString()\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "c279f8aa-dce6-4462-81e4-9aa7173e43e9",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "disabled": true,
      "position": [
        -288,
        -6528
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "483ccf8a-0dcb-4070-b653-23a644c8c003",
      "name": "\ud83d\udd27 Build Config",
      "type": "n8n-nodes-base.code",
      "position": [
        496,
        -5904
      ],
      "parameters": {
        "jsCode": "// ====================================================\n// \u2699\ufe0f ONLY EDIT THIS \u2014 your n8n instance URL\n// ====================================================\n\nconst n8n_url = 'https://stuctstunter.zeabur.app';\n\n// ====================================================\n// \ud83d\uded1 DO NOT EDIT BELOW\n// ====================================================\n\nconst rows = $input.all().map(i => i.json);\n\nif (rows.length === 0) {\n  throw new Error('Team_Config DataTable is empty. Run Layer 0A first.');\n}\n\nconst config = { n8n_url };\n\nfor (const row of rows) {\n  const tier = (row.tier_id || '').toLowerCase().trim();\n  config[tier + '_inbox'] = row.inbox_address;\n  config[tier + '_client_id'] = row.client_id;\n  config[tier + '_display_name'] = row.display_name;\n  config[tier + '_priority'] = row.priority;\n  config[tier + '_escalates_to'] = row.escalates_to || '';\n}\n\nreturn [{ json: config }];"
      },
      "typeVersion": 2
    },
    {
      "id": "c757b8b4-9fe6-406f-8607-180913fac2fb",
      "name": "Note: Layer 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        480,
        -6160
      ],
      "parameters": {
        "width": 1088,
        "height": 520,
        "content": "## \ud83c\udfd7\ufe0f LAYER 1: Registering the Webhook \n\n**Reads Team_Config DataTable \u2014 same source as Layer 0A.**\n\nBuilds config from DataTable rows, checks webhooks, registers missing ones.\n\nSet your n8n URL here\n"
      },
      "typeVersion": 1
    },
    {
      "id": "3670ef9a-a9fd-410c-9449-8cf6efd65dce",
      "name": "\u2699\ufe0f Load Team_Config",
      "type": "n8n-nodes-base.dataTable",
      "disabled": true,
      "position": [
        -48,
        -6528
      ],
      "parameters": {
        "limit": 20,
        "filters": {
          "conditions": [
            {
              "condition": "isNotEmpty"
            }
          ]
        },
        "orderBy": true,
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "ZPtmczWG8ZNDbAS6",
          "cachedResultUrl": "/projects/2qxSXp3XpPDzCYbp/datatables/ZPtmczWG8ZNDbAS6",
          "cachedResultName": "Team_Config"
        },
        "orderByColumn": "priority"
      },
      "typeVersion": 1.1
    },
    {
      "id": "6376c8c7-4891-4e51-9670-f4220de4b7a2",
      "name": "Note: Layer ",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        480,
        -5648
      ],
      "parameters": {
        "color": 4,
        "width": 1088,
        "height": 376,
        "content": "## \ud83d\udcca LAYER 4: Audit Dashboard\n\n**Purpose:** Pull all block lists across all 3 inboxes. See who is blocked, why, and when.\n\n**When to run:**\n- After any enforcement to verify\n- Scheduled (cron) for daily reports\n- On-demand by SDR managers\n\n**Endpoint:** `GET /v0/inboxes/{inbox_id}/lists/send/block`\n\n**This replaces looking at HubSpot.**\nThe block list IS the source of truth for email permissions."
      },
      "typeVersion": 1
    },
    {
      "id": "eabf5591-1fbd-44fa-ad08-43df539436cb",
      "name": "Note: Decay Timer",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1568,
        -7312
      ],
      "parameters": {
        "color": 3,
        "width": 1904,
        "height": 468,
        "content": "## \u23f3 LAYER 2: Decay Timer\n\n**Runs every Sunday at midnight.**\n\nReads Team_Config DataTable for inbox addresses AND decay windows.\nMAKE sure you setup the database first\nThe DataTable returns one row per inbox \u2014 that IS the fan out.\n\n```\nSTEP 1 \u2192 Read Team_Config (inbox + decay_days per tier)\nSTEP 2 \u2192 GET block list for each inbox\nSTEP 3 \u2192 Find entries older than that tier's decay_days\nSTEP 4 \u2192 DELETE expired blocks\nSTEP 5 \u2192 Compile report\n```\n\nPer-tier decay means AE blocks can last longer than Marketing blocks.\nEdit decay_days in the DataTable to change windows."
      },
      "typeVersion": 1
    },
    {
      "id": "b7c21d76-7603-4985-9ad7-95f27dc78e8d",
      "name": "\u2699\ufe0f Read Team_Config",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        1840,
        -6992
      ],
      "parameters": {
        "limit": 20,
        "filters": {
          "conditions": [
            {
              "condition": "isNotEmpty"
            }
          ]
        },
        "orderBy": true,
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "ZPtmczWG8ZNDbAS6",
          "cachedResultUrl": "/projects/2qxSXp3XpPDzCYbp/datatables/ZPtmczWG8ZNDbAS6",
          "cachedResultName": "Team_Config"
        },
        "orderByColumn": "priority"
      },
      "typeVersion": 1.1
    },
    {
      "id": "026be386-12c1-48c4-a032-fe1eac47c6a7",
      "name": "\ud83d\udcca Decay Report",
      "type": "n8n-nodes-base.code",
      "position": [
        2976,
        -7152
      ],
      "parameters": {
        "jsCode": "const removed = $input.all().map(i => i.json);\n\nconst byTier = {};\nfor (const item of removed) {\n  const tier = item.tier_id || 'unknown';\n  if (!byTier[tier]) byTier[tier] = { display_name: item.display_name, count: 0, prospects: [] };\n  byTier[tier].count++;\n  byTier[tier].prospects.push(item.prospect);\n}\n\nconst summaryLines = Object.entries(byTier).map(([tier, data]) => \n  '- ' + data.display_name + ' (' + tier + '): ' + data.count + ' leads released'\n);\n\nreturn [{ json: {\n  status: 'DECAY_COMPLETE',\n  total_released: removed.length,\n  by_tier: byTier,\n  message: 'Decay Timer Complete. Released ' + removed.length + ' prospects back into the TAM.',\n  summary: summaryLines.join('\\n'),\n  timestamp: new Date().toISOString()\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "286ff967-3c16-443c-9929-d4d4bcdd0b5e",
      "name": "\ud83d\udccb All Clear",
      "type": "n8n-nodes-base.code",
      "position": [
        2752,
        -6976
      ],
      "parameters": {
        "jsCode": "return [{ json: { status: 'CLEAN', message: 'All block lists within decay limits. Nothing to clear.', timestamp: new Date().toISOString() } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "b4f1b85e-ddd2-4420-8786-7de9c207c7d5",
      "name": "Note: Overview1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        480,
        -6848
      ],
      "parameters": {
        "color": 7,
        "width": 1824,
        "height": 700,
        "content": "# \ud83c\udfd7\ufe0f LAYER 0: Inbox Setup \n**Reads config from Team_Config DataTable.**\nNo hardcoded inbox names anywhere.\n\n```\nSTEP 1 \u2192 Read Team_Config table (source of truth)\nSTEP 2 \u2192 Fetch live inboxes from AgentMail\nSTEP 3 \u2192 Compare + tier check\nSTEP 4 \u2192 Route: ALL_MATCHED / CREATE / TIER_BLOCKED\nSTEP 5 \u2192 Create missing / Report\n```\n\nEdit the **Team_Config DataTable** to change inboxes.\nEdit **plan_tier** in the Compare node if you upgrade."
      },
      "typeVersion": 1
    },
    {
      "id": "d019203c-7d6f-4b8a-853c-6c75697e369e",
      "name": "\ud83d\udce5 Fetch Live Inboxes",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        576,
        -6416
      ],
      "parameters": {
        "url": "https://api.agentmail.to/v0/inboxes",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "27fe04bb-8f89-42cb-a5a6-422ad7e7a3f3",
      "name": "\ud83d\udd0d Compare + Tier Check",
      "type": "n8n-nodes-base.code",
      "position": [
        736,
        -6416
      ],
      "parameters": {
        "jsCode": "// ====================================================\n// \u2699\ufe0f PLAN TIER \u2014 only thing you edit here\n// ====================================================\n\nconst plan_tier = 'free';\nconst TIER_LIMITS = { free: 3, pro: 50, enterprise: 500 };\nconst max_inboxes = TIER_LIMITS[plan_tier] || 3;\n\n// ====================================================\n// \ud83d\uded1 DO NOT EDIT BELOW \u2014 reads from DataTable + API\n// ====================================================\n\n// Load Team_Config rows (already one item per row)\nconst configRows = $('\u2699\ufe0f Load Team_Config').all().map(i => i.json);\n\nif (configRows.length === 0) {\n  throw new Error('Team_Config DataTable is empty. Add your inbox rows first.');\n}\n\nif (configRows.length > max_inboxes) {\n  throw new Error('Team_Config has ' + configRows.length + ' inboxes but ' + plan_tier + ' plan allows ' + max_inboxes + '.');\n}\n\n// Build wanted list from DataTable\nconst wanted = configRows.map(row => {\n  const addr = (row.inbox_address || '').toLowerCase().trim();\n  const parts = addr.split('@');\n  return {\n    username: parts[0] || '',\n    domain: parts[1] || 'agentmail.to',\n    display_name: (row.display_name || '').trim(),\n    client_id: (row.client_id || '').trim(),\n    tier_id: (row.tier_id || '').trim(),\n    priority: row.priority || 0,\n    escalates_to: (row.escalates_to || '').trim(),\n    expected_inbox_id: addr\n  };\n});\n\n// Read live inboxes from API\nconst liveResponse = $input.first().json;\nconst liveInboxes = Array.isArray(liveResponse.inboxes) ? liveResponse.inboxes : Array.isArray(liveResponse) ? liveResponse : [];\nconst liveIds = new Set(liveInboxes.map(i => String(i.inbox_id || '').toLowerCase().trim()));\nconst wantedIds = new Set(wanted.map(w => w.expected_inbox_id));\n\nconst matched = [];\nconst missing = [];\n\nfor (const w of wanted) {\n  if (liveIds.has(w.expected_inbox_id)) { matched.push(w); }\n  else { missing.push(w); }\n}\n\nconst orphaned = liveInboxes\n  .filter(i => { const id = String(i.inbox_id || '').toLowerCase().trim(); return id && !wantedIds.has(id); })\n  .map(i => ({ inbox_id: String(i.inbox_id).toLowerCase().trim(), display_name: i.display_name || null, client_id: i.client_id || null }));\n\nconst slots_used = liveInboxes.length;\nconst slots_available = Math.max(0, max_inboxes - slots_used);\nconst freed_if_cleaned = orphaned.length;\nconst slots_after_cleanup = Math.max(0, max_inboxes - (slots_used - freed_if_cleaned));\nconst can_create = missing.length <= slots_available;\nconst can_create_after_cleanup = missing.length <= slots_after_cleanup;\n\nlet action;\nif (missing.length === 0) { action = 'ALL_MATCHED'; }\nelse if (can_create) { action = 'CREATE'; }\nelse { action = 'TIER_BLOCKED'; }\n\nreturn [{ json: { plan: { tier: plan_tier, max_inboxes, slots_used, slots_available, freed_if_cleaned, slots_after_cleanup, can_create, can_create_after_cleanup }, action, wanted_count: wanted.length, live_count: liveInboxes.length, matched_count: matched.length, missing_count: missing.length, orphaned_count: orphaned.length, matched, missing, orphaned } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "a90e4517-d58e-4fab-91f0-2fa48291cb7e",
      "name": "\ud83d\udd00 Route Decision",
      "type": "n8n-nodes-base.switch",
      "position": [
        960,
        -6432
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "all-matched",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "ALL_MATCHED"
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "create",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "CREATE"
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "blocked",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "TIER_BLOCKED"
                  }
                ]
              }
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.4
    },
    {
      "id": "7d5d6817-2fc5-46f1-af12-1ec082b5dede",
      "name": "\ud83d\udd00 Fan Out Missing",
      "type": "n8n-nodes-base.code",
      "position": [
        1136,
        -6528
      ],
      "parameters": {
        "jsCode": "const compareResult = $('\ud83d\udd0d Compare + Tier Check').first().json;\nconst missing = compareResult.missing || [];\nreturn missing.map(i => ({ json: i }));"
      },
      "typeVersion": 2
    },
    {
      "id": "71f7f095-c0ba-450e-88a5-10abebe99b71",
      "name": "\ud83d\udcec Create Missing Inboxes",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1360,
        -6528
      ],
      "parameters": {
        "url": "https://api.agentmail.to/v0/inboxes",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "jsonBody": "={\n  \"username\": \"{{ $json.username }}\",\n  \"display_name\": \"{{ $json.display_name }}\",\n  \"client_id\": \"{{ $json.client_id }}\"\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "ce8740ad-d746-4abf-a25a-ac22fa3fe49d",
      "name": "\ud83d\udd17 Aggregate Creations",
      "type": "n8n-nodes-base.code",
      "position": [
        1584,
        -6528
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nreturn [{ json: { creation_results: items.map(i => i.json), count: items.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "070c63b6-fc41-4031-8a32-f39d679a7d17",
      "name": "\ud83d\udccb Final Fetch",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1808,
        -6464
      ],
      "parameters": {
        "url": "https://api.agentmail.to/v0/inboxes",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "1d9b703e-1da7-4a85-bf5d-72aac06c548f",
      "name": "\u2705 Provisioning Report",
      "type": "n8n-nodes-base.code",
      "position": [
        1968,
        -6464
      ],
      "parameters": {
        "jsCode": "const liveResponse = $input.first().json;\nconst liveInboxes = Array.isArray(liveResponse.inboxes) ? liveResponse.inboxes : Array.isArray(liveResponse) ? liveResponse : [];\nconst liveIds = new Set(liveInboxes.map(i => String(i.inbox_id || '').toLowerCase().trim()));\n\nconst compareResult = $('\ud83d\udd0d Compare + Tier Check').first().json;\nconst wanted = [...compareResult.matched, ...compareResult.missing];\n\nconst confirmed = [];\nconst still_failed = [];\n\nfor (const inbox of wanted) {\n  if (liveIds.has(inbox.expected_inbox_id)) { confirmed.push(inbox.expected_inbox_id); }\n  else { still_failed.push(inbox.expected_inbox_id); }\n}\n\nconst allOk = still_failed.length === 0;\n\nreturn [{ json: { status: allOk ? 'PROVISIONING_COMPLETE' : 'PROVISIONING_PARTIAL', plan: compareResult.plan, wanted: wanted.length, confirmed_live: confirmed.length, failed_count: still_failed.length, confirmed, still_failed, orphaned: compareResult.orphaned || [], orphaned_count: compareResult.orphaned_count || 0, next_step: allOk ? ((compareResult.orphaned_count || 0) > 0 ? 'All wanted inboxes live. Delete orphaned inboxes then run Layer 0B.' : 'All inboxes live. Run Layer 1 to register webhooks.') : 'Some inboxes failed. Check AgentMail credentials.', timestamp: new Date().toISOString() } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "1db20e91-339f-423c-b848-3b0ea190f3f3",
      "name": "\u26d4 Tier Limit Report",
      "type": "n8n-nodes-base.code",
      "position": [
        1152,
        -6336
      ],
      "parameters": {
        "jsCode": "const data = $('\ud83d\udd0d Compare + Tier Check').first().json;\nconst plan = data.plan;\nconst missing = data.missing || [];\nconst orphaned = data.orphaned || [];\nconst orphanedIds = orphaned.map(o => o.inbox_id);\n\nlet resolution;\nif (plan.can_create_after_cleanup) {\n  resolution = { action_required: 'DELETE_ORPHANED_THEN_RETRY', instructions: ['1. Copy the orphaned inbox IDs below.', '2. Paste into \u2699\ufe0f Delete Config node.', '3. Run \u25b6\ufe0f Run Cleanup.', '4. Run \u25b6\ufe0f Run Provisioning again.'], inboxes_to_delete: orphanedIds, slots_freed: plan.freed_if_cleaned, will_succeed: plan.slots_after_cleanup >= missing.length };\n} else {\n  resolution = { action_required: 'UPGRADE_PLAN', instructions: ['Your ' + plan.tier + ' plan allows ' + plan.max_inboxes + ' inboxes.', 'You need ' + missing.length + ' new slots but only ' + plan.slots_after_cleanup + ' available after cleanup.', 'Upgrade your plan or reduce rows in Team_Config.'], will_succeed: false };\n}\n\nreturn [{ json: { status: 'TIER_BLOCKED', plan, missing: missing.map(m => m.expected_inbox_id), orphaned, resolution, timestamp: new Date().toISOString() } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "46d77930-ee2a-4b0a-8133-3e4a6db09a49",
      "name": "Note: Layer 2A Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1568,
        -6160
      ],
      "parameters": {
        "color": 7,
        "width": 1900,
        "height": 532,
        "content": "## \ud83d\udd12 LAYER 2: Hierarchy Enforcement \n**All config comes from Team_Config DataTable.**\n\n**Two webhook triggers in one workflow:**\n\n`message.sent` \u2192 Block lower-tier inboxes from that prospect\n`message.received` \u2192 Classify reply, detect signal, escalate upward\n\n**The hierarchy is driven by `priority` in Team_Config:**\n```\npriority 3 (AE)  sends \u2192 blocks priority 2 + 1\npriority 2 (BDR) sends \u2192 blocks priority 1\npriority 1 (MKT) sends \u2192 blocks nobody\n```\n\n**Escalation is driven by `escalates_to` in Team_Config:**\n```\nReply hits Marketing \u2192 escalates_to: bdr\nReply hits BDR       \u2192 escalates_to: ae\nReply hits AE        \u2192 escalates_to: (empty = top tier)\n```\n"
      },
      "typeVersion": 1
    },
    {
      "id": "5aa968fc-c155-481c-82ea-3d460032c5b4",
      "name": "\u26a1 Webhook: message.sent",
      "type": "n8n-nodes-base.webhook",
      "position": [
        2160,
        -5920
      ],
      "parameters": {
        "path": "agentmail-sent",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "6696e896-538c-4ac8-95cb-1936e9975f1a",
      "name": "\u2699\ufe0f Read Team_Config (sent)",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        2384,
        -5920
      ],
      "parameters": {
        "limit": 20,
        "orderBy": true,
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "ZPtmczWG8ZNDbAS6",
          "cachedResultUrl": "/projects/2qxSXp3XpPDzCYbp/datatables/ZPtmczWG8ZNDbAS6",
          "cachedResultName": "Team_Config"
        },
        "orderByColumn": "priority"
      },
      "typeVersion": 1.1
    },
    {
      "id": "2166def2-94ad-4413-9da6-541120f561ee",
      "name": "\ud83e\udde0 Calculate Hierarchy Blocks",
      "type": "n8n-nodes-base.code",
      "position": [
        2608,
        -5920
      ],
      "parameters": {
        "jsCode": "// ============================================\n// HIERARCHY ENFORCEMENT \u2014 Reads Team_Config\n// ============================================\n// $input = DataTable rows (config)\n// Webhook payload = named reference to webhook node\n// ============================================\n\n// 1. Read config from DataTable (comes via $input)\nconst configRows = $input.all().map(i => i.json);\n\nif (configRows.length === 0) {\n  return [{ json: { action: 'SKIP', reason: 'Team_Config DataTable is empty.' } }];\n}\n\n// 2. Read webhook payload from the WEBHOOK NODE (not $input)\nconst webhookData = $('\u26a1 Webhook: message.sent').first().json;\nconst body = webhookData.body || webhookData;\nconst event = body.event_type ? body : (body.body || body);\n\nif (!event.send || event.event_type !== 'message.sent') {\n  return [{ json: { action: 'SKIP', reason: 'Not a message.sent event. Got: ' + (event.event_type || 'unknown'), debug_keys: Object.keys(event) } }];\n}\n\n// 3. Build teams lookup from DataTable\nconst teams = {};\nfor (const row of configRows) {\n  teams[row.tier_id] = {\n    inbox: (row.inbox_address || '').toLowerCase(),\n    priority: row.priority || 0,\n    display_name: row.display_name || row.tier_id\n  };\n}\n\n// 4. Build inbox-to-tier lookup\nconst inboxToTier = {};\nfor (const [tierId, data] of Object.entries(teams)) {\n  inboxToTier[data.inbox] = tierId;\n}\n\n// 5. Parse sender and recipients\nconst send = event.send;\nconst senderInbox = (send.inbox_id || '').toLowerCase();\nconst prospects = (send.recipients || []).map(r => r.toLowerCase().trim());\n\nif (!senderInbox || prospects.length === 0) {\n  return [{ json: { action: 'SKIP', reason: 'Missing inbox_id or recipients.', event_id: event.event_id } }];\n}\n\nconst senderTierId = inboxToTier[senderInbox];\nif (!senderTierId) {\n  return [{ json: { action: 'SKIP', reason: 'Sender \"' + send.inbox_id + '\" not in Team_Config. Known inboxes: ' + Object.keys(inboxToTier).join(', '), event_id: event.event_id } }];\n}\n\nconst senderPriority = teams[senderTierId].priority;\n\n// 6. Find all tiers with lower priority\nconst lowerTiers = Object.entries(teams)\n  .filter(([tid, data]) => data.priority < senderPriority)\n  .map(([tid, data]) => ({ tier_id: tid, ...data }));\n\nif (lowerTiers.length === 0) {\n  return [{ json: { action: 'NONE', sender: send.inbox_id, sender_team: senderTierId, prospects, event_id: event.event_id, message: senderTierId.toUpperCase() + ' is lowest active tier. No blocks needed.' } }];\n}\n\n// 7. Generate block items\nconst blocks = [];\nfor (const prospect of prospects) {\n  for (const lower of lowerTiers) {\n    blocks.push({ json: {\n      action: 'BLOCK',\n      inbox_id: lower.inbox,\n      blocked_team: lower.tier_id,\n      prospect_email: prospect,\n      sender_inbox: send.inbox_id,\n      sender_team: senderTierId,\n      thread_id: send.thread_id,\n      message_id: send.message_id,\n      event_id: event.event_id,\n      reason: 'HIERARCHY_LOCK: ' + senderTierId.toUpperCase() + ' (' + send.inbox_id + ') engaged ' + prospect + '. ' + lower.tier_id.toUpperCase() + ' blocked. [event:' + event.event_id + ']'\n    } });\n  }\n}\n\nreturn blocks.length > 0 ? blocks : [{ json: { action: 'NONE', sender: send.inbox_id, sender_team: senderTierId, event_id: event.event_id, message: 'No blocks to execute.' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "152a7142-d60b-49f3-9414-4646587d9049",
      "name": "Blocks Needed?",
      "type": "n8n-nodes-base.if",
      "position": [
        2832,
        -5920
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "1",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.action }}",
              "rightValue": "BLOCK"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "ddcfd656-1c68-41c6-b8f7-c375ad1f10f8",
      "name": "\ud83d\udd12 Execute Block",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3056,
        -5984
      ],
      "parameters": {
        "url": "={{ 'https://api.agentmail.to/v0/inboxes/' + $json.inbox_id + '/lists/send/block' }}",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "jsonBody": "={\n  \"entry\": \"{{ $json.prospect_email }}\",\n  \"reason\": \"{{ $json.reason }}\"\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "4bd05047-91b0-449d-932e-0620ce005f39",
      "name": "\ud83d\udccb Enforcement Log",
      "type": "n8n-nodes-base.code",
      "position": [
        3248,
        -5984
      ],
      "parameters": {
        "jsCode": "const blocks = $input.all();\nconst calcItems = $('\ud83e\udde0 Calculate Hierarchy Blocks').all().filter(i => i.json.action === 'BLOCK');\nconst results = blocks.map((b, i) => {\n  const meta = calcItems[i] ? calcItems[i].json : {};\n  return { inbox_blocked: meta.inbox_id || 'unknown', team_blocked: meta.blocked_team || 'unknown', prospect: meta.prospect_email || 'unknown', success: b.json.scope_key || b.json.entry ? true : false };\n});\nconst first = calcItems[0] ? calcItems[0].json : {};\nreturn [{ json: { status: 'HIERARCHY_ENFORCED', trigger: 'message.sent webhook', sender: first.sender_inbox || 'unknown', sender_team: first.sender_team || 'unknown', event_id: first.event_id || 'unknown', blocks_executed: results.length, details: results, timestamp: new Date().toISOString() } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "7d4a1632-174d-47f0-a149-d2971d4ac27c",
      "name": "\ud83d\udccb No Action Log",
      "type": "n8n-nodes-base.code",
      "position": [
        3056,
        -5856
      ],
      "parameters": {
        "jsCode": "return [{ json: { status: 'NO_ACTION', trigger: 'message.sent webhook', reason: $json.reason || $json.message || 'Nothing to block.', sender: $json.sender || 'unknown', team: $json.sender_team || 'unknown', event_id: $json.event_id || 'unknown', timestamp: new Date().toISOString() } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "b2d7e135-b307-4953-bedf-ea16deeedc95",
      "name": "\u26a1 Webhook: message.received",
      "type": "n8n-nodes-base.webhook",
      "position": [
        2096,
        -5504
      ],
      "parameters": {
        "path": "agentmail-received",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "fc06c612-1612-427c-aee9-eecbc9c2962a",
      "name": "\u2699\ufe0f Read Team_Config (received)",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        2240,
        -5504
      ],
      "parameters": {
        "limit": 20,
        "orderBy": true,
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "ZPtmczWG8ZNDbAS6",
          "cachedResultUrl": "/projects/2qxSXp3XpPDzCYbp/datatables/ZPtmczWG8ZNDbAS6",
          "cachedResultName": "Team_Config"
        },
        "orderByColumn": "priority"
      },
      "typeVersion": 1.1
    },
    {
      "id": "89aad437-060e-4115-a7ed-aca6af6097cf",
      "name": "Note: Config Cleanup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2272,
        -6848
      ],
      "parameters": {
        "width": 1200,
        "height": 688,
        "content": "## \ud83d\uddd1\ufe0f CLEANUP (Config-Driven)\n\nDeletes orphaned inboxes automatically.\n\nSource of truth = Team_Config DataTable.\n\nFlow:\n1. Read Team_Config (desired inboxes)\n2. Fetch live inboxes from AgentMail\n3. Identify orphaned inboxes\n4. Delete only orphaned ones\n\n\u26a0\ufe0f Permanent deletion.\nNo manual list editing required. (good for being lazy)"
      },
      "typeVersion": 1
    },
    {
      "id": "f410506c-c53c-45b7-9a8e-ded82c72777d",
      "name": "\ud83e\udde0 Identify Orphaned",
      "type": "n8n-nodes-base.code",
      "position": [
        2576,
        -6432
      ],
      "parameters": {
        "jsCode": "// Compare DataTable vs Live\nconst configRows = $('\u2699\ufe0f Load Team_Config').all().map(i => i.json);\nconst live = $input.first().json;\nconst liveInboxes = Array.isArray(live.inboxes) ? live.inboxes : live;\n\nconst desired = new Set(configRows.map(r => (r.inbox_address || '').toLowerCase()));\n\nconst orphaned = liveInboxes\n  .map(i => (i.inbox_id || '').toLowerCase())\n  .filter(id => id && !desired.has(id));\n\nif (orphaned.length === 0) {\n  return [{ json: { action: 'NONE', message: 'No orphaned inboxes found.' } }];\n}\n\nreturn orphaned.map(id => ({\n  json: {\n    action: 'DELETE',\n    inbox_id: id,\n    url: 'https://api.agentmail.to/v0/inboxes/' + id\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "5ce70a0a-a215-4af3-9861-6bdf5ab8e57c",
      "name": "Delete Needed?",
      "type": "n8n-nodes-base.if",
      "position": [
        2800,
        -6432
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "conditions": [
            {
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.action }}",
              "rightValue": "DELETE"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "4feab3b8-52de-4259-86dc-858ceebc6342",
      "name": "\ud83d\uddd1\ufe0f DELETE Inbox",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3024,
        -6432
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "method": "DELETE",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "f4d1a784-adbe-45d5-98f8-c0c75af89af7",
      "name": "\ud83d\udccb Cleanup Report",
      "type": "n8n-nodes-base.code",
      "position": [
        3248,
        -6432
      ],
      "parameters": {
        "jsCode": "const deleted = $input.all().map(i => i.json.inbox_id);\nreturn [{ json: {\n  status: 'CLEANUP_COMPLETE',\n  deleted_count: deleted.length,\n  deleted,\n  timestamp: new Date().toISOString()\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "b010a556-a522-43df-899b-3c9f6d85ed09",
      "name": "\ud83e\udde0 Smart Classify & Route",
      "type": "n8n-nodes-base.code",
      "position": [
        2384,
        -5504
      ],
      "parameters": {
        "jsCode": "// ============================================\n// ENHANCED ROUTING \u2014 Labels + Shielding\n// ============================================\nconst configRows = $input.all().map(i => i.json);\nconst webhookData = $('\u26a1 Webhook: message.received').first().json;\n\nconst msg = webhookData.body?.message || webhookData.message;\nif (!msg || !msg.inbox_id) return [{ json: { action: 'SKIP', reason: 'Invalid payload' } }];\n\n// 1. Identify Receiver\nconst receiver = configRows.find(c => c.inbox_address === msg.inbox_id);\nif (!receiver) return [{ json: { action: 'SKIP', reason: 'Inbox not in Team_Config' } }];\n\n// 2. Signal Check\nconst body = (msg.extracted_text || msg.text || \"\").toLowerCase();\nlet signal = 'NEUTRAL';\nif (['interested', 'call', 'pricing', 'demo', 'schedule'].some(k => body.includes(k))) signal = 'HOT';\nif (['stop', 'unsubscribe', 'remove'].some(k => body.includes(k))) signal = 'COLD';\n\n// 3. Routing Decision\nlet action = 'PROCESS'; \nlet label_to_add = `reply-${signal.toLowerCase()}`;\n// Block the receiver (e.g., Marketing) from sending more automated emails unless the lead is cold (unsubscribe)\nlet block_needed = (signal !== 'COLD' && receiver.tier_id !== 'ae');\n\nreturn [{\n  json: {\n    action,\n    signal,\n    label_to_add,\n    block_needed,\n    inbox_to_block: receiver.inbox_address,\n    prospect: msg.from,\n    thread_id: msg.thread_id,\n    message_id: msg.message_id,\n    receiving_tier: receiver.tier_id\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "db8d8bb9-7bca-4a8a-8851-f78248981daf",
      "name": "\ud83d\udd00 Route by Action",
      "type": "n8n-nodes-base.switch",
      "position": [
        2528,
        -5504
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "process",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "PROCESS"
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "skip",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "SKIP"
                  }
                ]
              }
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.4
    },
    {
      "id": "ab61dbe6-37bc-4066-8bc9-5a69bd367ba7",
      "name": "\ud83c\udff7\ufe0f Apply State Label",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2672,
        -5616
      ],
      "parameters": {
        "url": "={{ 'https://api.agentmail.to/v0/inboxes/' + $json.inbox_to_block + '/messages/' + $json.message_id }}",
        "method": "PATCH",
        "options": {},
        "jsonBody": "={\n  \"add_labels\": [\"{{ $json.label_to_add }}\", \"needs-review\"],\n  \"remove_labels\": [\"unread\"]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "52f79061-1a7b-4989-9e1c-0aee212efc81",
      "name": "Shield Needed?",
      "type": "n8n-nodes-base.if",
      "position": [
        2688,
        -5424
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "block-check",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.block_needed }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "bf408e6d-e7d8-4ebd-9bb1-0f8c5d5f544a",
      "name": "\ud83d\udd12 Execute Reply Shield",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2848,
        -5440
      ],
      "parameters": {
        "url": "={{ 'https://api.agentmail.to/v0/inboxes/' + $json.inbox_to_block + '/lists/send/block' }}",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "jsonBody": "={\n  \"entry\": \"{{ $json.prospect }}\",\n  \"reason\": \"AUTO-SHIELD: Prospect replied ({{ $json.signal }}). Halting automated sequences.\"\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "112f8ab4-58e4-4716-9918-dae94644149c",
      "name": "\ud83d\udce8 Pipeline Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1568,
        -5648
      ],
      "parameters": {
        "color": "#E8F5E9",
        "width": 1896,
        "height": 380,
        "content": "## \ud83d\udce8 Reply Shield \u2014 `message.received` Pipeline\n\n**What this does:**\nWhen a prospect replies to ANY inbox on your team, this flow:\n1. **Classifies** the reply signal (HOT / NEUTRAL / COLD)\n2. **Labels** the message in AgentMail for instant visual context\n3. **Blocks** future automated sends from that inbox to the prospect\n\n**Why it matters:**\nA prospect who says \"let's jump on a call\" should NEVER receive\na generic nurture drip 2 hours later. This flow enforces silence\nthe instant a human conversation begins.\n\n**Trigger:** AgentMail `message.received` webhook\n"
      },
      "typeVersion": 1
    },
    {
      "id": "5e50c396-f7f4-44b9-bafb-036198eca374",
      "name": "\ud83d\udd00 Router Reference",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3008,
        -5568
      ],
      "parameters": {
        "color": "#E8F5E9",
        "width": 440,
        "height": 264,
        "content": "## \ud83d\udd00 Route by Action \u2014 Switch Node\n\n**Output 0 \u2192 `PROCESS`**\nValid reply from a known inbox.\nForks to BOTH paths in parallel:\n  \u2192 \ud83c\udff7\ufe0f Apply State Label (always)\n  \u2192 Shield Needed? check (always)\n\n**Output 1 \u2192 `SKIP`**\nInvalid payload or unknown inbox.\nDead end \u2014 workflow stops.\nNo API calls, no logs, no noise."
      },
      "typeVersion": 1
    },
    {
      "id": "8079bb92-26c5-4791-a4e4-abb0acea0d7d",
      "name": "\ud83e\udde0 Planning Engine",
      "type": "n8n-nodes-base.code",
      "position": [
        848,
        -5904
      ],
      "parameters": {
        "jsCode": "const cfg = $('\ud83d\udd27 Build Config').first().json;\nconst liveWebhooks = $input.first().json.webhooks || [];\n\nconst workOrder = [];\n\nconst expectedHooks = [\n  { url: cfg.n8n_url + '/webhook/agentmail-sent', event: 'message.sent' },\n  { url: cfg.n8n_url + '/webhook/agentmail-received', event: 'message.received' }\n];\n\nfor (const hook of expectedHooks) {\n  const exists = liveWebhooks.some(w => w.url === hook.url && w.event_types && w.event_types.includes(hook.event));\n  if (!exists) {\n    workOrder.push({ action: 'CREATE_WEBHOOK', webhook_url: hook.url, event: hook.event });\n  }\n}\n\nif (workOrder.length === 0) {\n  return [{ json: { action: 'ALL_CLEAR', message: 'All webhooks registered.', config_summary: cfg, live_webhooks: liveWebhooks.map(w => ({ url: w.url, events: w.event_types })) } }];\n}\n\nreturn workOrder.map(task => ({ json: task }));"
      },
      "typeVersion": 2
    },
    {
      "id": "841e2153-82e9-4695-a2af-134083ec8022",
      "name": "GET Webhooks",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        672,
        -5904
      ],
      "parameters": {
        "url": "https://api.agentmail.to/v0/webhooks",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "32174e33-6481-4b07-87d5-304a954c4d1d",
      "name": "Route by Action",
      "type": "n8n-nodes-base.switch",
      "position": [
        1040,
        -5904
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "hook",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "CREATE_WEBHOOK"
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "clear",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "ALL_CLEAR"
                  }
                ]
              }
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.4
    },
    {
      "id": "3b975b67-d823-4840-b985-ce3834503fac",
      "name": "\u2705 System Ready",
      "type": "n8n-nodes-base.code",
      "position": [
        1264,
        -5808
      ],
      "parameters": {
        "jsCode": "const cfg = $('\ud83d\udd27 Build Config').first().json;\n\nreturn [{ json: { status: 'LAYER_1_COMPLETE', message: 'All webhooks registered.', config: cfg, next_step: 'Run Layer 0B to validate full stack.', timestamp: new Date().toISOString() } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "2048db1d-6da6-4d07-a5a3-3e3e540e0c19",
      "name": "\ud83d\udce1 Register Webhook",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1264,
        -6000
      ],
      "parameters": {
        "url": "https://api.agentmail.to/v0/webhooks",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"url\": \"{{ $json.webhook_url }}\",\n  \"event_types\": [\"{{ $json.event }}\"]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "c6ff7dfb-9833-4e30-922c-2192477cb734",
      "name": "\ud83e\uddf9 Clear Block",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2752,
        -7152
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "method": "DELETE",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "982046c5-1a71-4e66-84e9-508e837b575b",
      "name": "Weekly (Sun 00:00) - or put it to ANYTHING you like",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        1616,
        -6992
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 0 * * 0"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "10118a45-134b-4548-b1bd-f8967bc314bd",
      "name": "GET blockLists",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2064,
        -6992
      ],
      "parameters": {
        "url": "={{ 'https://api.agentmail.to/v0/inboxes/' + $json.inbox_address + '/lists/send/block' }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "251e4524-8712-457e-8ad4-6a6ac3ebf52a",
      "name": "\ud83e\udde0 Find Expired Entries",
      "type": "n8n-nodes-base.code",
      "position": [
        2288,
        -6992
      ],
      "parameters": {
        "jsCode": "const responses = $input.all();\nconst configRows = $('\u2699\ufe0f Read Team_Config').all();\nconst now = new Date();\nconst expired = [];\n\nfor (let i = 0; i < responses.length; i++) {\n  const meta = configRows[i] ? configRows[i].json : {};\n  const resp = responses[i].json;\n  const entries = Array.isArray(resp.entries) ? resp.entries : Array.isArray(resp) ? resp : [];\n  const decayDays = meta.decay_days || 30;\n\n  for (const entry of entries) {\n    if (!entry.created_at) continue;\n\n    const blockDate = new Date(entry.created_at);\n    const ageDays = (now - blockDate) / (1000 * 60 * 60 * 24);\n\n    if (ageDays > decayDays) {\n      expired.push({\n        json: {\n          action: 'DELETE',\n          inbox_id: meta.inbox_address,\n          tier_id: meta.tier_id,\n          display_name: meta.display_name,\n          prospect: entry.entry,\n          age_days: Math.round(ageDays),\n          decay_threshold: decayDays,\n          url: 'https://api.agentmail.to/v0/inboxes/' + meta.inbox_address + '/lists/send/block/' + encodeURIComponent(entry.entry)\n        }\n      });\n    }\n  }\n}\n\nif (expired.length === 0) {\n  return [{ json: { action: 'NONE', message: 'No expired blocks found. All entries within decay windows.' } }];\n}\n\nreturn expired;"
      },
      "typeVersion": 2
    },
    {
      "id": "d76f0b35-ba0d-4f64-8f5e-67465b74c02c",
      "name": "Blocks to Clear?",
      "type": "n8n-nodes-base.if",
      "position": [
        2512,
        -6992
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "del",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.action }}",
              "rightValue": "DELETE"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "fd682d51-0d84-4299-9c16-ac42043db24b",
      "name": "\ud83d\udce5 Fetch Live inboxes.",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2352,
        -6432
      ],
      "parameters": {
        "url": "https://api.agentmail.to/v0/inboxes",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "28e449f2-28e5-440e-a305-f2606eae8e8a",
      "name": "Note: Database & Trigger Logic1",
      "type": "n8n-nodes-base.stickyNote",
      "disabled": true,
      "position": [
        -352,
        -6672
      ],
      "parameters": {
        "color": "#FFC107",
        "width": 512,
        "height": 352,
        "content": "## USE this as your manual TEST trigger. But make sure you run the database setup first.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "234aa99e-6311-454b-8e08-a91e9e85859c",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1344,
        -7264
      ],
      "parameters": {
        "color": 7,
        "width": 160,
        "height": 80,
        "content": "# STEP 1\n"
      },
      "typeVersion": 1
    },
    {
      "id": "3ea3f28f-9177-48a5-9773-595e402b1508",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1872,
        -6720
      ],
      "parameters": {
        "color": 7,
        "width": 160,
        "height": 80,
        "content": "# STEP 2\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "30db289e-5da5-4426-bd95-26d6547ec337",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1296,
        -6112
      ],
      "parameters": {
        "color": 7,
        "width": 160,
        "height": 80,
        "content": "# STEP 3\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c5970675-04ca-47ce-a63b-c754c7d98090",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3168,
        -6720
      ],
      "parameters": {
        "color": 7,
        "width": 208,
        "height": 80,
        "content": "# OPTIONAL\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c73f2318-c6e8-4bc0-8c7e-6e0f7ed308ea",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2416,
        -7296
      ],
      

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This is for SaaS founders, agency owners, and Sales Ops managers who use HubSpot but are tired of "toe-stepping." If your BDRs are accidentally emailing your AE’s active deals, or Marketing is blasting "Closed Won" accounts with generic newsletters, your CRM has failed you. This…

Source: https://n8n.io/workflows/15122/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

Get your AI agents paying for resources autonomously in under 10 minutes.

Data Table, HTTP Request
AI & RAG

Who is this for? Agencies, consultants, and service providers who conduct discovery calls and need to quickly turn conversations into professional proposals.

Tool Think, Tool Calculator, Agent Tool +18
AI & RAG

Main. Uses httpRequest, agent, lmChatGoogleGemini, outputParserStructured. Event-driven trigger; 57 nodes.

HTTP Request, Agent, Google Gemini Chat +4
AI & RAG

This is an automated blog post generation system that: Researches topics using AI agents and web search tools Writes complete blog posts with proper SEO structure Generates custom images for each post

Output Parser Structured, Google Gemini Chat, HTTP Request Tool +11
AI & RAG

Receives campaign parameters via form, creates a Smartlead campaign, sources qualified leads through Wiza based on your ICP description, researches each prospect with Perplexity AI, generates personal

HTTP Request, Output Parser Structured, Memory Buffer Window +6