{
  "name": "TrueFetch Job Search API — collect or resume",
  "nodes": [
    {
      "id": "start",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {}
    },
    {
      "id": "config",
      "name": "Config",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "parameters": {
        "jsCode": "// Host configuration. Generate and persist a canonical key OUTSIDE this node before a NEW intent.\n// Keep mode='recover' after any uncertain response. Never generate a key in an execution expression.\nconst config={mode:'recover',requestKey:'REPLACE_WITH_ORIGINAL_CANONICAL_KEY',taskId:'',input:{keyword:'Software engineer',platforms:['arbeitnow'],maxResults:10,maxCredits:100,allowPartial:false,country:'Germany'}};\nif(!['new','recover'].includes(config.mode))throw Error('Choose new intent or original command recovery');\nif(config.taskId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(config.taskId))throw Error('Invalid recovery task ID');\nif(!config.taskId&&!/^v1\\.[1-9][0-9]{12}\\.[A-Za-z0-9_-]{22,64}$/.test(config.requestKey))throw Error('Set and retain the original canonical command key');\nif(!config.taskId&&config.mode==='new'&&(!Number.isSafeInteger(config.input.maxCredits)||config.input.maxCredits<1||config.input.maxCredits>1000000000))throw Error('Set a positive integer credit budget');\nreturn [{json:config}];"
      }
    },
    {
      "id": "resume-existing-task",
      "name": "Resume existing task",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        440,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "Resume existing task",
              "leftValue": "={{ $json.taskId !== '' }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "recovery-handle",
      "name": "Recovery handle",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        -240
      ],
      "parameters": {
        "jsCode": "return [{json:{task:{taskId:$json.taskId}}}];"
      }
    },
    {
      "id": "estimate",
      "name": "Estimate",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        140
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.truefetch.io/v1/quotes",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({productId:\"job-search\",input: $(\"Config\").first().json.input}) }}"
      }
    },
    {
      "id": "check-budget",
      "name": "Check budget",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        140
      ],
      "parameters": {
        "jsCode": "const estimate=$json.estimate, config=$('Config').first().json, input=config.input;\nconst units=value=>typeof value==='string'&&/^[1-9][0-9]{0,19}$/.test(value);\nif(estimate?.productId!=='job-search'||!units(estimate.reservedUnits)||!units(estimate.unitPriceUnits)||typeof estimate.pricingVersion!=='string'||!estimate.pricingVersion||!estimate.input||typeof estimate.input!=='object'||Array.isArray(estimate.input))throw Error('Invalid estimate');\nfor(const [field,requested] of Object.entries(input)){\n if(field==='platforms'&&Array.isArray(requested)&&!requested.length)continue;\n const expected=field==='keyword'&&typeof requested==='string'?requested.trim():field==='platforms'&&Array.isArray(requested)?[...requested].sort():requested;\n const observed=field==='platforms'&&Array.isArray(estimate.input[field])?[...estimate.input[field]].sort():estimate.input[field];\n if(!Object.hasOwn(estimate.input,field)||JSON.stringify(observed)!==JSON.stringify(expected))throw Error('Estimate input does not match the requested collection');\n}\nif(!Number.isSafeInteger(estimate.resultLimit)||estimate.resultLimit<1||estimate.resultLimit>1000||!Array.isArray(estimate.input.platforms)||!estimate.input.platforms.length||new Set(estimate.input.platforms).size!==estimate.input.platforms.length||estimate.input.platforms.some(site=>!['arbeitnow','linkedin','job_bank'].includes(site))||estimate.resultLimit!==input.maxResults*estimate.input.platforms.length||BigInt(estimate.reservedUnits)!==BigInt(estimate.resultLimit)*BigInt(estimate.unitPriceUnits))throw Error('Estimate price and reservation are inconsistent');\nif(!Number.isSafeInteger(input.maxCredits)||input.maxCredits<1||input.maxCredits>1000000000)throw Error('Set a positive integer credit budget');\nif(BigInt(estimate.reservedUnits)>BigInt(input.maxCredits)*1000000n)throw Error('Estimate exceeds configured credit budget');\nif(typeof estimate.expiresAt!=='string'||!Number.isFinite(Date.parse(estimate.expiresAt))||Date.parse(estimate.expiresAt)<=Date.now())throw Error('Estimate expired; obtain a fresh estimate with the same intent');\nreturn [{json:{productId:'job-search',input,expectedPricingVersion:estimate.pricingVersion}}];"
      }
    },
    {
      "id": "create-task",
      "name": "Create task",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        140
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.truefetch.io/v1/tasks",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Idempotency-Key",
              "value": "={{ $(\"Config\").first().json.requestKey }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}"
      }
    },
    {
      "id": "read-task",
      "name": "Read task",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1320,
        -240
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://api.truefetch.io/v1/tasks/\" + encodeURIComponent($json.task.taskId) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "check-task",
      "name": "Check task",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        140
      ],
      "parameters": {
        "jsCode": "const task=$json.task;\nconst config=$(\"Config\").first().json;\nconst expected=(config.taskId || $(\"Check original command\").first().json.task.taskId).toLowerCase();\nif(task?.taskId!==expected)throw Error(\"Response does not match the requested original task\");\nif(task?.productId!=='job-search'||typeof task.taskId!=='string'||!/^[0-9a-f-]{36}$/i.test(task.taskId))throw Error('Invalid task response');\nconst pending=['queued','running','finalizing','retry_wait'].includes(task.status);\nif(pending&&$runIndex>=60)throw Error('Polling limit reached; resume using task ID '+task.taskId+' instead of starting a new task');\nif(!pending&&!['succeeded','partial','empty'].includes(task.status))throw Error('Task ended without deliverable results: '+task.status+'; task ID '+task.taskId);\nif(!pending&&task.resultAvailability!=='available')throw Error('Saved results unavailable: '+task.resultAvailability+'; task ID '+task.taskId);\nreturn [{json:{task,pending}}];"
      }
    },
    {
      "id": "still-running",
      "name": "Still running",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1540,
        140
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "Still running",
              "leftValue": "={{ $json.pending }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "wait-before-poll",
      "name": "Wait before poll",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1540,
        -240
      ],
      "parameters": {
        "amount": 5,
        "unit": "seconds"
      }
    },
    {
      "id": "begin-pages",
      "name": "Begin pages",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        140
      ],
      "parameters": {
        "jsCode": "const task=$json.task;\nif(!Number.isSafeInteger(task.resultCount)||task.resultCount<0)throw Error('Invalid result count');\nreturn [{json:{task,cursor:null,seenCursors:[],rows:[]}}];"
      }
    },
    {
      "id": "page-state",
      "name": "Page state",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        140
      ],
      "parameters": {
        "jsCode": "return $input.all();"
      }
    },
    {
      "id": "read-page",
      "name": "Read page",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2200,
        140
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://api.truefetch.io/v1/tasks/\" + encodeURIComponent($json.task.taskId) + \"/results?pageSize=100\" + ($json.cursor ? \"&cursor=\" + encodeURIComponent($json.cursor) : \"\") }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "validate-page",
      "name": "Validate page",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2420,
        140
      ],
      "parameters": {
        "jsCode": "const state=$('Page state').first(0,$runIndex).json, page=$json, pagination=page.meta?.pagination;\nif(!Array.isArray(page.data)||page.meta?.resultCount!==state.task.resultCount||typeof pagination?.hasMore!=='boolean')throw Error('Invalid result page');\nlet previous=state.rows.at(-1)?.recordId;\nfor(const row of page.data){if(typeof row?.recordId!=='string'||!row.recordId||(previous&&row.recordId<=previous))throw Error('Duplicate or out-of-order result');previous=row.recordId;}\nconst rows=[...state.rows,...page.data];\nif(rows.length>state.task.resultCount)throw Error('Too many result records');\nconst cursor=pagination.nextCursor;\nif(pagination.hasMore){\n if(!page.data.length||typeof cursor!=='string'||!cursor||state.seenCursors.includes(cursor)||rows.length>=state.task.resultCount)throw Error('Pagination cannot make progress');\n}else if(cursor!==null||rows.length!==state.task.resultCount)throw Error('Incomplete result export');\nreturn [{json:{...state,rows,cursor,seenCursors:cursor?[...state.seenCursors,cursor]:state.seenCursors,hasMore:pagination.hasMore}}];"
      }
    },
    {
      "id": "read-receipt",
      "name": "Read receipt",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2640,
        140
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://api.truefetch.io/v1/tasks/\" + encodeURIComponent($json.task.taskId) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "check-receipt",
      "name": "Check receipt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2860,
        140
      ],
      "parameters": {
        "jsCode": "const state=$('Validate page').first(0,$runIndex).json, current=$json.task, original=state.task;\nif(current?.taskId!==original.taskId||current.productId!=='job-search'||current.status!==original.status||current.resultAvailability!=='available'||!Number.isSafeInteger(current.resultCount)||current.resultCount!==original.resultCount)throw Error('Task changed during result export; recover the same task ID '+original.taskId);\nreturn [{json:state}];"
      }
    },
    {
      "id": "more-pages",
      "name": "More pages",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3080,
        140
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "More pages",
              "leftValue": "={{ $json.hasMore }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "complete",
      "name": "Complete",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3300,
        300
      ],
      "parameters": {
        "jsCode": "return [{json:{task:$json.task,data:$json.rows,complete:true,partial:$json.task.status==='partial',sourceOutcomes:$json.task.sourceOutcomes,coverageTruncated:$json.task.coverageTruncated}}];"
      }
    },
    {
      "id": "new-intent",
      "name": "New intent?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        480,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "Resume existing task",
              "leftValue": "={{ $('Config').first().json.mode === 'new' }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "read-original-command",
      "name": "Read original command",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        700,
        450
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ \"https://api.truefetch.io/v1/commands/tasks.create/\" + encodeURIComponent($(\"Config\").first().json.requestKey) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      }
    },
    {
      "id": "check-original-command",
      "name": "Check original command",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1150,
        300
      ],
      "parameters": {
        "jsCode": "const receipt=$json.receipt;\nif(!receipt||receipt.kind!=='tasks.create'||!['accepted','processing','rejected','unknown','not_accepted_final','receipt_expired'].includes(receipt.outcome))throw Error('Invalid original command receipt');\nif(receipt.outcome!=='accepted'){\n if(receipt.result!==null)throw Error('Unaccepted command contains result references');\n const next=receipt.outcome==='receipt_expired'?'Acceptance cannot be determined; inspect Tasks or contact support.':receipt.outcome==='rejected'?'Read the recorded reason before authorizing new work.':receipt.outcome==='not_accepted_final'?'Original was sealed without acceptance; authorize any new intent separately.':'Acceptance remains unconfirmed; explicitly read this original receipt again.';\n throw Error('Original command '+receipt.outcome+'. '+next);\n}\nconst refs=receipt.result?.objects;if(!Array.isArray(refs)||refs.length>20)throw Error('Invalid accepted references');\nconst tasks=refs.filter(item=>item&&item.kind==='task');\nif(tasks.length!==1||typeof tasks[0].id!=='string'||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tasks[0].id))throw Error('Accepted command needs one original task reference');\nconst taskId=tasks[0].id.toLowerCase();\nif($json.task!==undefined&&$json.task!==null&&($json.task.taskId!==taskId||$json.task.productId!=='job-search'))throw Error('Task does not match accepted receipt');\nreturn [{json:{receipt,task:{taskId}}}];"
      }
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Resume existing task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resume existing task": {
      "main": [
        [
          {
            "node": "Recovery handle",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "New intent?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Recovery handle": {
      "main": [
        [
          {
            "node": "Read task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Estimate": {
      "main": [
        [
          {
            "node": "Check budget",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check budget": {
      "main": [
        [
          {
            "node": "Create task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create task": {
      "main": [
        [
          {
            "node": "Check original command",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read task": {
      "main": [
        [
          {
            "node": "Check task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check task": {
      "main": [
        [
          {
            "node": "Still running",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Still running": {
      "main": [
        [
          {
            "node": "Wait before poll",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Begin pages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait before poll": {
      "main": [
        [
          {
            "node": "Read task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Begin pages": {
      "main": [
        [
          {
            "node": "Page state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Page state": {
      "main": [
        [
          {
            "node": "Read page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read page": {
      "main": [
        [
          {
            "node": "Validate page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate page": {
      "main": [
        [
          {
            "node": "Read receipt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "More pages": {
      "main": [
        [
          {
            "node": "Page state",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Complete",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read receipt": {
      "main": [
        [
          {
            "node": "Check receipt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check receipt": {
      "main": [
        [
          {
            "node": "More pages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "New intent?": {
      "main": [
        [
          {
            "node": "Estimate",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Read original command",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read original command": {
      "main": [
        [
          {
            "node": "Check original command",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check original command": {
      "main": [
        [
          {
            "node": "Read task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 600,
    "saveDataSuccessExecution": "none",
    "saveDataErrorExecution": "none",
    "saveManualExecutions": false
  }
}
