AutomationFlowsWeb Scraping › Torah Router

Torah Router

Torah Router. Uses httpRequest. Webhook trigger; 25 nodes.

Webhook trigger★★★★☆ complexity25 nodesHTTP Request
Web Scraping Trigger: Webhook Nodes: 25 Complexity: ★★★★☆ Added:

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
{
  "name": "Torah Router",
  "active": true,
  "nodes": [
    {
      "parameters": {
        "content": "## Torah Router v2.0\n\n**Endpoint:** `POST /webhook/torah-router`\n\n**BREAKING CHANGES v2.0:**\n- `kind` field REQUIRED ('segment' | 'commentary')\n- No silent fallback to pending_translations\n- Strict UUID validation\n\n**Validation:**\n- kind='segment' \u2192 segment_id UUID required\n- kind='commentary' \u2192 commentary_detail_id UUID required\n- Missing/invalid \u2192 HTTP 400 (no fallback)\n\n**Pipelines:**\n- Batch: segments[] \u2192 Translate each \u2192 Save\n- Single: text \u2192 Translate \u2192 Save\n- Long text: Chunk \u2192 Translate \u2192 Save",
        "height": 380,
        "width": 340,
        "color": 5
      },
      "id": "sticky-doc",
      "name": "Documentation",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        48,
        48
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "torah-router",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook-trigger",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        448,
        304
      ]
    },
    {
      "parameters": {
        "jsCode": "// Torah Router v2.0 - Strict Validation\n// NO silent fallback - reject invalid payloads with HTTP 400\n\nconst input = $input.first().json;\nconst body = input.body || input;\nconst headers = input.headers || {};\n\nconst CHUNK_THRESHOLD = 10000;\nconst UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\nconst VALID_KINDS = ['segment', 'commentary'];\n\n// Helper: validate UUID\nconst isValidUUID = (str) => str && UUID_REGEX.test(str);\n\n// Helper: create error response\nconst errorResponse = (code, message) => [{\n  json: {\n    valid: false,\n    error: { code: 400, errorCode: code, message: message }\n  }\n}];\n\n// ============================================\n// VALIDATION v2.0: Strict, no fallback\n// ============================================\n\n// 1. Validate 'kind' field (REQUIRED)\nconst kind = body.kind;\nif (!kind) {\n  return errorResponse('MISSING_KIND', \"Field 'kind' is required. Must be 'segment' or 'commentary'.\");\n}\nif (!VALID_KINDS.includes(kind)) {\n  return errorResponse('INVALID_KIND', `Invalid kind '${kind}'. Must be 'segment' or 'commentary'.`);\n}\n\n// 2. Validate conditional IDs based on kind\nif (kind === 'segment') {\n  const segmentId = body.segment_id;\n  if (!segmentId) {\n    return errorResponse('MISSING_SEGMENT_ID', \"Field 'segment_id' is required when kind='segment'.\");\n  }\n  if (!isValidUUID(segmentId)) {\n    return errorResponse('INVALID_UUID', `Field 'segment_id' must be a valid UUID. Got: '${segmentId}'.`);\n  }\n}\n\nif (kind === 'commentary') {\n  const commentaryDetailId = body.commentary_detail_id;\n  if (!commentaryDetailId) {\n    return errorResponse('MISSING_COMMENTARY_DETAIL_ID', \"Field 'commentary_detail_id' is required when kind='commentary'.\");\n  }\n  if (!isValidUUID(commentaryDetailId)) {\n    return errorResponse('INVALID_UUID', `Field 'commentary_detail_id' must be a valid UUID. Got: '${commentaryDetailId}'.`);\n  }\n}\n\n// 3. Validate api_key\nif (!body.api_key) {\n  return errorResponse('MISSING_API_KEY', \"Field 'api_key' is required in the request body.\");\n}\n\n// 4. Validate text or segments\nif (!body.text && (!body.segments || !Array.isArray(body.segments) || body.segments.length === 0)) {\n  return errorResponse('MISSING_TEXT', \"Either 'text' or 'segments[]' is required.\");\n}\n\n// ============================================\n// PARSING: Build internal structure\n// ============================================\n\nconst jobIdProvided = !!body.job_id;\nconst jobId = body.job_id || 'job_' + Date.now().toString(36) + Math.random().toString(36).substring(2, 8);\nconst projectId = headers['x-project-id'] || body.project_id || 'default';\nconst requestId = body.request_id || 'req_' + Date.now().toString(36);\n\nlet pipelineType = 'unknown';\nlet segments = [];\nlet needsChunking = false;\nlet totalSegments = 0;\n\nif (body.segments && Array.isArray(body.segments) && body.segments.length > 0) {\n  // Batch mode: multiple segments\n  pipelineType = 'batch';\n  \n  // Validate each segment has required ID based on kind\n  for (let idx = 0; idx < body.segments.length; idx++) {\n    const s = body.segments[idx];\n    \n    if (kind === 'segment' && !isValidUUID(s.segment_id)) {\n      return errorResponse('INVALID_SEGMENT_IN_BATCH', `Segment[${idx}]: 'segment_id' must be a valid UUID when kind='segment'.`);\n    }\n    if (kind === 'commentary' && !isValidUUID(s.commentary_detail_id)) {\n      return errorResponse('INVALID_SEGMENT_IN_BATCH', `Segment[${idx}]: 'commentary_detail_id' must be a valid UUID when kind='commentary'.`);\n    }\n  }\n  \n  segments = body.segments.map((s, idx) => {\n    const text = typeof s === 'string' ? s : (s.text || s.text_hebrew || s.hebrew_text || '');\n    return {\n      index: idx,\n      text: text,\n      segment_id: kind === 'segment' ? s.segment_id : null,\n      commentary_detail_id: kind === 'commentary' ? s.commentary_detail_id : null,\n      reference_translation: s.reference_translation || null,\n      char_count: text.length\n    };\n  });\n  \n  // Check max length\n  const tooLongSegment = segments.find(s => s.char_count > CHUNK_THRESHOLD);\n  if (tooLongSegment) {\n    return errorResponse('SEGMENT_TOO_LONG', `Segment ${tooLongSegment.index} is too long (${tooLongSegment.char_count} chars). Max: ${CHUNK_THRESHOLD}.`);\n  }\n  \n  totalSegments = segments.length;\n  \n} else if (body.text) {\n  // Single text mode\n  const textLength = body.text.length;\n  \n  if (textLength > CHUNK_THRESHOLD) {\n    pipelineType = 'chunk_then_translate';\n    needsChunking = true;\n    totalSegments = 1; // Will be updated after chunking\n  } else {\n    pipelineType = 'translate_single';\n    totalSegments = 1;\n  }\n  \n  segments = [{\n    index: 0,\n    text: body.text,\n    segment_id: kind === 'segment' ? body.segment_id : null,\n    commentary_detail_id: kind === 'commentary' ? body.commentary_detail_id : null,\n    reference_translation: body.reference_translation || null,\n    char_count: textLength\n  }];\n}\n\n// ============================================\n// OUTPUT: Validated and parsed data\n// ============================================\n\nreturn [{\n  json: {\n    valid: true,\n    // Routing\n    kind: kind,\n    jobId: jobId,\n    jobIdProvided: jobIdProvided,\n    needsJobCreation: !jobIdProvided,\n    projectId: projectId,\n    requestId: requestId,\n    // Pipeline\n    jobType: body.job_type || 'translation',\n    pipelineType: pipelineType,\n    needsChunking: needsChunking,\n    // Segments\n    segments: segments,\n    totalSegments: totalSegments,\n    // Context\n    traite: body.traite || body.context?.traite,\n    page: body.page || body.context?.page,\n    section: body.section || body.context?.section,\n    commentator: body.commentator || body.context?.commentator,\n    // Languages\n    sourceLanguage: body.source_language || 'he',\n    targetLanguage: body.target_language || 'fr',\n    // Auth\n    apiKey: body.api_key,\n    openaiApiKey: body.openai_api_key,\n    // Metadata\n    context: body.context || {},\n    metadata: body.metadata || {},\n    chunkThreshold: CHUNK_THRESHOLD\n  }\n}];"
      },
      "id": "parse-input",
      "name": "Parse Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        672,
        304
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.valid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "check-valid",
      "name": "Valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        880,
        304
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.N8N_WEBHOOK_URL || 'http://localhost:5678' }}/webhook/torah-error",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ job_id: 'validation_rejection_' + Date.now(), segment_index: 0, worker: 'torah-router-validation', error: { code: $json.error?.errorCode || 'VALIDATION_ERROR', message: $json.error?.message || 'Unknown validation error' }, context: { validation_failed: true, error_code: $json.error?.errorCode, rejected_at: new Date().toISOString() } }) }}",
        "options": {
          "timeout": 5000
        }
      },
      "id": "log-rejected-payload",
      "name": "Log Rejected Payload",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1104,
        512
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, error: { code: $('Parse Input').first().json.error.errorCode, message: $('Parse Input').first().json.error.message } }) }}",
        "options": {
          "responseCode": "={{ $('Parse Input').first().json.error?.code || 400 }}"
        }
      },
      "id": "respond-error",
      "name": "Respond Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1328,
        512
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: true, job_id: $json.jobId, kind: $json.kind, pipeline: $json.pipelineType, segments_count: $json.totalSegments }) }}",
        "options": {
          "responseCode": 202
        }
      },
      "id": "respond-accepted",
      "name": "Respond Accepted",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1104,
        208
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $('Parse Input').first().json.needsJobCreation }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "needs-job-creation",
      "name": "Needs Job Creation?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1328,
        208
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.API_URL }}/api/v2/jobs",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Project-ID",
              "value": "={{ $('Parse Input').first().json.projectId }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ job_id: $('Parse Input').first().json.jobId, job_type: $('Parse Input').first().json.jobType, kind: $('Parse Input').first().json.kind, status: 'pending', input: { traite: $('Parse Input').first().json.traite, page: $('Parse Input').first().json.page, target_language: $('Parse Input').first().json.targetLanguage, segments_count: $('Parse Input').first().json.totalSegments, commentator: $('Parse Input').first().json.commentator } }) }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "create-job",
      "name": "Create Job",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1552,
        112
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "={{ $env.API_URL }}/api/v2/jobs/{{ $('Parse Input').first().json.jobId }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ status: 'processing', progress: { current: 0, total: $('Parse Input').first().json.totalSegments, percentage: 0 } }) }}",
        "options": {
          "timeout": 5000
        }
      },
      "id": "set-processing",
      "name": "Set Processing",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1760,
        320
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $('Parse Input').first().json.needsChunking }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "needs-chunking",
      "name": "Needs Chunking?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1552,
        272
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.N8N_WEBHOOK_URL || 'http://localhost:5678' }}/webhook/torah-chunk",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ job_id: $('Parse Input').first().json.jobId, text: $('Parse Input').first().json.segments[0].text, segment_id: $('Parse Input').first().json.segments[0].segment_id, commentary_detail_id: $('Parse Input').first().json.segments[0].commentary_detail_id, kind: $('Parse Input').first().json.kind, threshold: $('Parse Input').first().json.chunkThreshold, api_key: $('Parse Input').first().json.apiKey, context: { traite: $('Parse Input').first().json.traite, page: $('Parse Input').first().json.page, section: $('Parse Input').first().json.section } }) }}",
        "options": {
          "timeout": 300000
        }
      },
      "id": "call-chunk-worker",
      "name": "Call Chunk Worker",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1760,
        112
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Prepare segments for translation loop (after chunking)\nconst input = $('Parse Input').first().json;\nconst chunkResult = $input.first().json;\n\nlet segments = [];\n\nif (chunkResult.segments && Array.isArray(chunkResult.segments)) {\n  segments = chunkResult.segments.map((s, idx) => ({\n    index: idx,\n    text: s.text,\n    segment_id: input.segments[0]?.segment_id,\n    commentary_detail_id: input.segments[0]?.commentary_detail_id,\n    char_count: s.text?.length || 0,\n    isChunk: true,\n    totalChunks: chunkResult.segments.length\n  }));\n} else {\n  segments = input.segments;\n}\n\nconst items = segments.map(segment => ({\n  json: {\n    ...segment,\n    kind: input.kind,\n    jobId: input.jobId,\n    totalSegments: segments.length,\n    traite: input.traite,\n    page: input.page,\n    section: input.section,\n    commentator: input.commentator,\n    sourceLanguage: input.sourceLanguage,\n    targetLanguage: input.targetLanguage,\n    apiKey: input.apiKey,\n    openaiApiKey: input.openaiApiKey,\n    context: input.context,\n    metadata: input.metadata,\n    pipelineType: input.pipelineType\n  }\n}));\n\nreturn items;"
      },
      "id": "prepare-segments",
      "name": "Prepare Segments",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1984,
        208
      ]
    },
    {
      "parameters": {
        "jsCode": "// Use original segments directly (no chunking)\nconst input = $('Parse Input').first().json;\n\nconst items = input.segments.map(segment => ({\n  json: {\n    ...segment,\n    kind: input.kind,\n    jobId: input.jobId,\n    totalSegments: input.totalSegments,\n    traite: input.traite,\n    page: input.page,\n    section: input.section,\n    commentator: input.commentator,\n    sourceLanguage: input.sourceLanguage,\n    targetLanguage: input.targetLanguage,\n    apiKey: input.apiKey,\n    openaiApiKey: input.openaiApiKey,\n    context: input.context,\n    metadata: input.metadata,\n    pipelineType: input.pipelineType\n  }\n}));\n\nreturn items;"
      },
      "id": "prepare-segments-direct",
      "name": "Prepare Segments Direct",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        672
      ]
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "loop-segments",
      "name": "Loop Segments",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        2432,
        208
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.N8N_WEBHOOK_URL || 'http://localhost:5678' }}/webhook/torah-translate",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ job_id: $json.jobId, segment_id: $json.segment_id, commentary_detail_id: $json.commentary_detail_id, kind: $json.kind, text: $json.text, reference_translation: $json.reference_translation, source_language: $json.sourceLanguage, target_language: $json.targetLanguage, api_key: $json.apiKey, context: { traite: $json.traite, page: $json.page, section: $json.section, commentator: $json.commentator }, metadata: $json.metadata }) }}",
        "options": {
          "timeout": 300000
        }
      },
      "id": "call-translate-worker",
      "name": "Call Translate Worker",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2640,
        304
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.success }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "translate-success",
      "name": "Translate Success?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2864,
        304
      ]
    },
    {
      "parameters": {
        "jsCode": "// Prepare save payload - route by kind, no source_text as identifier\nconst loopItem = $('Loop Segments').first().json;\nconst translateResult = $input.first().json;\n\n// Base payload - use correct ID field based on kind\nconst targetPayload = {\n  jobId: loopItem.jobId,\n  kind: loopItem.kind,\n  segmentIndex: loopItem.index,\n  totalSegments: loopItem.totalSegments,\n  translation: translateResult.translation,\n  // ID based on kind (mutually exclusive)\n  segment_id: loopItem.kind === 'segment' ? loopItem.segment_id : null,\n  commentary_detail_id: loopItem.kind === 'commentary' ? loopItem.commentary_detail_id : null,\n  // NO source_text as identifier - it was causing ambiguous payload errors\n  targetLanguage: loopItem.targetLanguage,\n  provider: 'anthropic',\n  model: 'claude-sonnet-4',\n  metadata: loopItem.metadata,\n  isChunk: loopItem.isChunk || false,\n  pipelineType: loopItem.pipelineType,\n  method: translateResult.method || 'unknown'\n};\n\n// Handle EN pivot generation\nconst hasGeneratedEnglish = translateResult.generated_english === true && translateResult.english_pivot;\n\nif (hasGeneratedEnglish) {\n  const englishPayload = {\n    ...targetPayload,\n    translation: translateResult.english_pivot,\n    targetLanguage: 'en',\n    metadata: { ...loopItem.metadata, is_generated_pivot: true },\n    pipelineType: 'en_pivot_generation',\n    method: 'structured_generation',\n    isEnglishPivot: true\n  };\n  \n  return [\n    { json: englishPayload },\n    { json: { ...targetPayload, hasEnglishPivotSaved: true } }\n  ];\n}\n\nreturn [{ json: targetPayload }];"
      },
      "id": "prepare-save",
      "name": "Prepare Save",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3088,
        208
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.N8N_WEBHOOK_URL || 'http://localhost:5678' }}/webhook/torah-save",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ kind: $json.kind, segment_id: $json.segment_id, commentary_detail_id: $json.commentary_detail_id, translated_text: $json.translation, target_language: $json.targetLanguage, provider: $json.provider || 'anthropic', model: $json.model || 'claude-sonnet-4', quality_score: $json.qualityScore, job_id: $json.jobId, request_id: $json.requestId, status: 'approved' }) }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "call-save-worker",
      "name": "Call Save Worker",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3312,
        208
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.N8N_WEBHOOK_URL || 'http://localhost:5678' }}/webhook/torah-error",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ job_id: $('Loop Segments').first().json.jobId, segment_index: $('Loop Segments').first().json.index, worker: 'translate', error: { code: $json.error?.code || 'TRANSLATE_ERROR', message: $json.error?.message || 'Translation failed' }, context: { kind: $('Loop Segments').first().json.kind, text_preview: $('Loop Segments').first().json.text?.substring(0, 100), char_count: $('Loop Segments').first().json.char_count } }) }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "call-error-handler",
      "name": "Call Error Handler",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3088,
        400
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Update progress after each segment\nconst loopItem = $('Loop Segments').first().json;\nconst current = loopItem.index + 1;\nconst total = loopItem.totalSegments;\nconst percentage = Math.round((current / total) * 100);\n\nreturn [{\n  json: {\n    jobId: loopItem.jobId,\n    current: current,\n    total: total,\n    percentage: percentage,\n    isLast: current >= total\n  }\n}];"
      },
      "id": "calc-progress",
      "name": "Calc Progress",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3520,
        304
      ]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "={{ $env.API_URL }}/api/v2/jobs/{{ $json.jobId }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ status: 'processing', progress: { current: $json.current, total: $json.total, percentage: $json.percentage } }) }}",
        "options": {
          "timeout": 5000
        }
      },
      "id": "update-progress",
      "name": "Update Progress",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3744,
        304
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $('Calc Progress').first().json.isLast }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "is-last",
      "name": "Is Last?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3968,
        304
      ]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "={{ $env.API_URL }}/api/v2/jobs/{{ $('Calc Progress').first().json.jobId }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ status: 'completed', progress: { current: $('Calc Progress').first().json.total, total: $('Calc Progress').first().json.total, percentage: 100 } }) }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "set-completed",
      "name": "Set Completed",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        4192,
        208
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {},
      "id": "done",
      "name": "Done",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        4400,
        304
      ]
    }
  ],
  "connections": {
    "Webhook Trigger": {
      "main": [
        [
          {
            "node": "Parse Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Input": {
      "main": [
        [
          {
            "node": "Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Valid?": {
      "main": [
        [
          {
            "node": "Respond Accepted",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Rejected Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Rejected Payload": {
      "main": [
        [
          {
            "node": "Respond Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond Accepted": {
      "main": [
        [
          {
            "node": "Needs Job Creation?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs Job Creation?": {
      "main": [
        [
          {
            "node": "Create Job",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Job": {
      "main": [
        [
          {
            "node": "Set Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Processing": {
      "main": [
        [
          {
            "node": "Needs Chunking?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs Chunking?": {
      "main": [
        [
          {
            "node": "Call Chunk Worker",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Segments Direct",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Chunk Worker": {
      "main": [
        [
          {
            "node": "Prepare Segments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Segments": {
      "main": [
        [
          {
            "node": "Loop Segments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Segments Direct": {
      "main": [
        [
          {
            "node": "Loop Segments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Segments": {
      "main": [
        [
          {
            "node": "Done",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Call Translate Worker",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Translate Worker": {
      "main": [
        [
          {
            "node": "Translate Success?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Translate Success?": {
      "main": [
        [
          {
            "node": "Prepare Save",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Call Error Handler",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Save": {
      "main": [
        [
          {
            "node": "Call Save Worker",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Save Worker": {
      "main": [
        [
          {
            "node": "Calc Progress",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Error Handler": {
      "main": [
        [
          {
            "node": "Calc Progress",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calc Progress": {
      "main": [
        [
          {
            "node": "Update Progress",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Progress": {
      "main": [
        [
          {
            "node": "Is Last?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Last?": {
      "main": [
        [
          {
            "node": "Set Completed",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Segments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Completed": {
      "main": [
        [
          {
            "node": "Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "callerPolicy": "workflowsFromSameOwner"
  },
  "staticData": null,
  "tags": []
}
Pro

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

About this workflow

Torah Router. Uses httpRequest. Webhook trigger; 25 nodes.

Source: https://github.com/fsebbah/n8n-workflows/blob/cfce483f81a240422fd048e1c680c746b877429a/workflows/Torah_Router.json — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

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

Web Scraping

This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di

n8n, Execute Workflow Trigger, HTTP Request +1
Web Scraping

This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .

HTTP Request, Ssh
Web Scraping

This workflow receives webhook requests from a content calendar and uses the X API v2 to publish text posts, threads, image/video posts, and polls, as well as delete existing posts and run a credentia

HTTP Request
Web Scraping

This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c

HTTP Request
Web Scraping

Sign PDF documents with legally-compliant digital signatures using X.509 certificates. Supports multiple PAdES signature levels (B, T, LT, LTA) with optional visible stamps.

Execute Command, HTTP Request, Read Write File +1