{
  "name": "Margo lead discovery - bounded polling and human review",
  "versionId": "6a639322-c536-4d9b-ab8f-a7fbfd5d2c66",
  "nodes": [
    {
      "id": "margo-1",
      "name": "Start manually",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "parameters": {},
      "position": [
        0,
        0
      ]
    },
    {
      "id": "margo-2",
      "name": "Configure search",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "parameters": {
        "jsCode": "return [{json: {method: 'POST', url: 'https://www.margoleads.io/api/v1/leads/generate', body: {source: 'google_maps', category: 'dentist', state: 'London', country: 'GB', maxResults: 10}, idempotencyKey: 'n8n-' + $execution.id + '-london-dentists', postRetriesEnabled: false, retries: 0, polls: 0}}];"
      }
    },
    {
      "id": "margo-3",
      "name": "Prepare request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all();"
      }
    },
    {
      "id": "margo-4",
      "name": "POST or poll",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        560,
        0
      ],
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.method }}",
              "operation": "equal",
              "value2": "POST"
            }
          ]
        },
        "combineOperation": "all"
      }
    },
    {
      "id": "margo-5",
      "name": "Margo POST",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        0
      ],
      "onError": "continueRegularOutput",
      "parameters": {
        "method": "POST",
        "url": "={{ $json.url }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Idempotency-Key",
              "value": "={{ $json.idempotencyKey }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "margo-6",
      "name": "Margo poll",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        160
      ],
      "onError": "continueRegularOutput",
      "parameters": {
        "method": "GET",
        "url": "={{ $json.url }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": false,
        "options": {
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "margo-7",
      "name": "Classify response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        0
      ],
      "parameters": {
        "jsCode": "function classify(response, state, now = Date.now()) {\n  const next = { ...state };\n  const status = Number(response.statusCode || 0);\n  const body = response.body ?? {};\n  if (!status || status === 429 || status >= 500) {\n    if (next.method === 'POST' && next.postRetriesEnabled !== true) throw new Error('POST retry paused: confirm the governed idempotent handler before enabling postRetriesEnabled. Inspect this search before starting another.');\n    if (next.retries >= 3) throw new Error('Retry limit reached; review this search before starting another.');\n    next.retries += 1;\n    const header = response.headers?.['retry-after'] ?? response.headers?.['Retry-After'];\n    const delay = header == null ? NaN : (/^\\d+(\\.\\d+)?$/.test(String(header)) ? Number(header) : (Date.parse(String(header)) - now) / 1000);\n    next.waitSeconds = Number.isFinite(delay) ? Math.max(1, Math.ceil(delay)) : Math.min(60, 2 ** next.retries * 5);\n    // Do not silently shorten a server-directed cooldown or wait indefinitely.\n    if (next.waitSeconds > 3600) throw new Error('Retry-After exceeds one hour; stop for review.');\n    return { ...next, action: 'wait' };\n  }\n  if (status < 200 || status >= 300) throw new Error(`HTTP ${status}: ${body.code ?? body.error ?? 'request rejected'}; review credentials, access, quota or request. No new search was started.`);\n  const terminal = body.terminal ?? body.job?.result?.terminal;\n  const jobStatus = terminal?.status ?? body.job?.status;\n  if (['FAILED', 'CANCELLED'].includes(jobStatus)) throw new Error(`Job ${jobStatus}: ${body.job?.errorCode ?? terminal?.stopReason ?? 'review required'}`);\n  if (status === 202 || ['QUEUED', 'RUNNING', 'PENDING'].includes(jobStatus)) {\n    if (next.polls >= 60) throw new Error('Polling limit reached; inspect the existing job before starting another.');\n    const pollUrl = String(body.pollUrl ?? '');\n    if (!/^\\/api\\/v1\\/leads\\/jobs\\/[a-zA-Z0-9_-]+$/.test(pollUrl)) throw new Error('Missing or unsafe pollUrl.');\n    return { ...next, method: 'GET', url: `https://www.margoleads.io${pollUrl}`, polls: next.polls + 1, retries: 0, waitSeconds: 5, action: 'wait' };\n  }\n  if (jobStatus && !['COMPLETE', 'PARTIAL', 'EMPTY'].includes(jobStatus)) throw new Error(`Unknown terminal status ${jobStatus}`);\n  const result = body.job ? body.job.result : body;\n  if (!result || !Array.isArray(result.leads)) throw new Error('Missing lead array; stop for review.');\n  const leads = jobStatus === 'EMPTY' ? [] : result.leads;\n  return {\n    action: 'review', status: jobStatus ?? (leads.length ? 'COMPLETE' : 'EMPTY'),\n    requestedCount: terminal?.requestedCount ?? next.body.maxResults,\n    deliveredCount: terminal?.deliveredCount ?? result.count ?? result.leadCount ?? leads.length,\n    stopReason: terminal?.stopReason ?? null,\n    resultSetId: terminal?.resultSetId ?? result.resultSetId ?? null,\n    limitations: body.limitations ?? result.limitations ?? [],\n    constraintCoverage: body.constraintCoverage ?? result.constraintCoverage ?? {},\n    leads, idempotencyKey: next.idempotencyKey, reviewRequired: true,\n  };\n}\nreturn [{json: classify($input.first().json, $('Prepare request').item.json)}];"
      }
    },
    {
      "id": "margo-8",
      "name": "Wait or review",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        1100,
        0
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.action === \"wait\" }}",
              "operation": "equal",
              "value2": true
            }
          ]
        },
        "combineOperation": "all"
      }
    },
    {
      "id": "margo-9",
      "name": "Wait",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1320,
        -120
      ],
      "parameters": {
        "resume": "timeInterval",
        "amount": "={{ $json.waitSeconds }}",
        "unit": "seconds"
      },
      "webhookId": "margo-leads-bounded-wait"
    },
    {
      "id": "margo-10",
      "name": "Human review receipt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        120
      ],
      "parameters": {
        "jsCode": "return $input.all(); // Inspect status, limitations and leads before adding any delivery node."
      }
    }
  ],
  "connections": {
    "Start manually": {
      "main": [
        [
          {
            "node": "Configure search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure search": {
      "main": [
        [
          {
            "node": "Prepare request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare request": {
      "main": [
        [
          {
            "node": "POST or poll",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "POST or poll": {
      "main": [
        [
          {
            "node": "Margo POST",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Margo poll",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Margo POST": {
      "main": [
        [
          {
            "node": "Classify response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Margo poll": {
      "main": [
        [
          {
            "node": "Classify response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify response": {
      "main": [
        [
          {
            "node": "Wait or review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait or review": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Human review receipt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait": {
      "main": [
        [
          {
            "node": "Prepare request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "tags": []
}
