{
  "name": "Profundis · client searches with a credit ceiling",
  "nodes": [
    {
      "id": "start",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {}
    },
    {
      "id": "configuration",
      "name": "Configuration",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        200,
        0
      ],
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"organization_id\": \"REPLACE_WITH_ORGANIZATION_UUID\",\n  \"client_id\": \"REPLACE_WITH_CLIENT_UUID\",\n  \"operation_key\": \"REPLACE_WITH_UNIQUE_OPERATION_KEY\",\n  \"query\": \"host:example.com\",\n  \"max_credits\": 5\n}"
      }
    },
    {
      "id": "validate-request",
      "name": "Validate request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        400,
        0
      ],
      "parameters": {
        "jsCode": "const c = $input.first().json;\nconst uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\nif (!uuid.test(c.organization_id) || !uuid.test(c.client_id)) throw new Error('Set the organization and client UUIDs in Configuration.');\nif (typeof c.operation_key !== 'string' || !/^[a-zA-Z0-9_.:-]{16,120}$/.test(c.operation_key) || c.operation_key.startsWith('REPLACE_')) throw new Error('Choose an operation key of 16–120 characters. Keep it unchanged when recovering this operation.');\nif (!Number.isSafeInteger(c.max_credits) || c.max_credits < 1 || c.max_credits > 1000000) throw new Error('Set an explicit positive credit ceiling.');\nif (typeof c.query !== 'string' || !c.query.trim() || c.query.length > 20000) throw new Error('Set a non-empty search query.');\nconst request = {max_credits:c.max_credits,items:['hosts','dns'].map(engine=>({id:engine,engine,query:{raw_query:c.query,results_per_page:30}}))};\nreturn [{json:{...c,request,base:'https://api.profundis.io/api/v2/service/organizations/'+c.organization_id+'/clients/'+c.client_id}}];"
      }
    },
    {
      "id": "submit-batch",
      "name": "Submit batch",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        600,
        0
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $('Validate request').first().json.base + '/batches' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 15000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Idempotency-Key",
              "value": "={{ $('Validate request').first().json.operation_key }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $('Validate request').first().json.request }}"
      }
    },
    {
      "id": "validate-batch",
      "name": "Validate batch",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        800,
        0
      ],
      "parameters": {
        "jsCode": "const b = $input.first().json;\nconst c = $('Validate request').first().json;\nif (!b.batch || b.batch.organization_id !== c.organization_id || b.batch.client_id !== c.client_id || !/^[0-9a-f-]{36}$/i.test(b.batch.id) || !Array.isArray(b.items) || b.items.length !== 2) throw new Error('Unexpected batch response. Keep the operation key for recovery.');\nreturn [{json:b}];"
      }
    },
    {
      "id": "read-batch",
      "name": "Read batch",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1000,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $('Validate request').first().json.base + '/batches/' + $('Validate batch').first().json.batch.id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 15000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "check-progress",
      "name": "Check progress",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        0
      ],
      "parameters": {
        "jsCode": "const b = $input.first().json;\nconst original = $('Validate batch').first().json;\nif (!b.batch || b.batch.id !== original.batch.id || b.batch.organization_id !== original.batch.organization_id || b.batch.client_id !== original.batch.client_id || !Array.isArray(b.items) || b.items.length !== original.items.length) throw new Error('Unexpected batch response.');\nfor (const item of b.items) {\n  if (!original.items.some(x => x.job_id === item.job_id && x.item_id === item.item_id) || !/^[0-9a-f-]{36}$/i.test(item.job_id) || !['queued','running','succeeded','failed','cancelled'].includes(item.state)) throw new Error('Unexpected job response.');\n}\nconst pending = b.items.some(x => ['queued','running'].includes(x.state));\nreturn [{json:{...b,polling_stopped:pending && $runIndex >= 59,keep_polling:pending && $runIndex < 59}}];"
      }
    },
    {
      "id": "keep-polling",
      "name": "Keep polling",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1400,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "condition",
              "leftValue": "={{ $json.keep_polling }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "wait-five-seconds",
      "name": "Wait five seconds",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1400,
        -200
      ],
      "parameters": {
        "amount": 5,
        "unit": "seconds"
      },
      "webhookId": "profundis-batch-poll"
    },
    {
      "id": "job-outcomes",
      "name": "Job outcomes",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1600,
        100
      ],
      "parameters": {
        "jsCode": "const b = $input.first().json;\nreturn b.items.map(item=>({json:{...item,batch_id:b.batch.id,organization_id:b.batch.organization_id,client_id:b.batch.client_id,polling_stopped:b.polling_stopped}}));"
      }
    },
    {
      "id": "succeeded",
      "name": "Succeeded",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1800,
        100
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "condition",
              "leftValue": "={{ $json.state === 'succeeded' }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "read-result",
      "name": "Read result",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2000,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $('Validate request').first().json.base + '/jobs/' + $json.job_id + '/result' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 15000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "needs-attention",
      "name": "Needs attention",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2000,
        200
      ],
      "parameters": {
        "jsCode": "return $input.all(); // Failed, cancelled or still running. No resubmission and no automatic budget increase."
      }
    },
    {
      "id": "setup-notes",
      "name": "Setup notes",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        200,
        -380
      ],
      "parameters": {
        "content": "# Profundis · bounded client searches\n\n1. Set client / organization IDs, query, maximum credits and a unique operation key. Reuse the key and exact request to recover an interrupted run.\n2. Bind the SAME stored HTTP Header Auth credential to all three HTTP nodes: Authorization = Bearer <organization service key>. Grant search:read for this client. Never paste secrets into this workflow.\n3. Start manually. Polling stops after 60 reads (~5 min + HTTP time); retain batch/job IDs to resume later.\n\nNo results are sent elsewhere. Review n8n execution retention and access before using client data. HTTP redirects are disabled. Import inactive; no personal-account fallback.",
        "height": 300,
        "width": 760
      }
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuration": {
      "main": [
        [
          {
            "node": "Validate request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate request": {
      "main": [
        [
          {
            "node": "Submit batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Submit batch": {
      "main": [
        [
          {
            "node": "Validate batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate batch": {
      "main": [
        [
          {
            "node": "Read batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read batch": {
      "main": [
        [
          {
            "node": "Check progress",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check progress": {
      "main": [
        [
          {
            "node": "Keep polling",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep polling": {
      "main": [
        [
          {
            "node": "Wait five seconds",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Job outcomes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait five seconds": {
      "main": [
        [
          {
            "node": "Read batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Job outcomes": {
      "main": [
        [
          {
            "node": "Succeeded",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Succeeded": {
      "main": [
        [
          {
            "node": "Read result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Needs attention",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 1200,
    "saveManualExecutions": false,
    "saveDataSuccessExecution": "none",
    "saveDataErrorExecution": "all"
  },
  "pinData": {},
  "tags": []
}
