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\n\n**Endpoint:** `POST /webhook/torah-router`\n\n**Responsabilite:**\n- Analyse le payload\n- Decide du pipeline (chunk/translate/save)\n- Orchestre les workers\n- Met a jour le progress\n- Gere les erreurs\n\n**Pipelines:**\n- Batch commentaires: Translate -> Save (pour chaque)\n- Texte long: Chunk -> Translate -> Save (combine)\n- Texte court: Translate -> Save",
        "height": 340,
        "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": "// Parse and validate input\nconst input = $input.first().json;\nconst body = input.body || input;\nconst headers = input.headers || {};\n\nconst CHUNK_THRESHOLD = 10000;\n\n// Generate job_id if not provided\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';\n\n// Determine pipeline type\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 (commentaries)\n  pipelineType = 'batch';\n  segments = body.segments.map((s, idx) => ({\n    index: idx,\n    text: typeof s === 'string' ? s : (s.text || s.text_hebrew || s.hebrew_text),\n    segment_id: s.segment_id || null,\n    commentary_id: s.commentary_id || null,\n    source_text: typeof s === 'string' ? s : null,\n    // RFC bilingual-translations-en-pivot: EN pivot for quality\n    reference_translation: s.reference_translation || null,\n    char_count: (typeof s === 'string' ? s : (s.text || s.text_hebrew || s.hebrew_text || '')).length\n  }));\n  \n  // Check if any segment is too long\n  const tooLongSegment = segments.find(s => s.char_count > CHUNK_THRESHOLD);\n  if (tooLongSegment) {\n    return [{\n      json: {\n        valid: false,\n        error: {\n          code: 400,\n          message: `Segment ${tooLongSegment.index} is too long (${tooLongSegment.char_count} chars). Max: ${CHUNK_THRESHOLD}. Use chunking for long texts.`\n        }\n      }\n    }];\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    // Segments will be created by Chunk Worker\n    segments = [{ index: 0, text: body.text, segment_id: body.segment_id || null, source_text: body.text, char_count: textLength }];\n    totalSegments = 1; // Will be updated after chunking\n  } else {\n    pipelineType = 'translate_single';\n    segments = [{\n      index: 0,\n      text: body.text,\n      segment_id: body.segment_id || null,\n      source_text: body.text,\n      char_count: textLength\n    }];\n    totalSegments = 1;\n  }\n} else {\n  return [{\n    json: {\n      valid: false,\n      error: { code: 400, message: 'Either segments[] or text is required' }\n    }\n  }];\n}\n\nreturn [{\n  json: {\n    valid: true,\n    jobId: jobId,\n    jobIdProvided: jobIdProvided,\n    needsJobCreation: !jobIdProvided,\n    projectId: projectId,\n    jobType: body.job_type || 'translation',\n    pipelineType: pipelineType,\n    needsChunking: needsChunking,\n    segments: segments,\n    totalSegments: totalSegments,\n    traite: body.traite,\n    page: body.page,\n    section: body.section,\n    commentator: body.commentator,\n    sourceLanguage: body.source_language || 'he',\n    targetLanguage: body.target_language || 'fr',\n    apiKey: body.api_key,\n    openaiApiKey: body.openai_api_key,\n    context: body.context || {},\n    metadata: body.metadata || {},\n    requestId: body.request_id,\n    model: body.model || 'claude-sonnet-4-6',\n    provider: body.provider || 'anthropic',\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": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, error: $json.error }) }}",
        "options": {
          "responseCode": "={{ $json.error?.code || 400 }}"
        }
      },
      "id": "respond-error",
      "name": "Respond Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1104,
        512
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ received: true, job_id: $json.jobId, pipeline: $json.pipelineType, segments_count: $json.totalSegments }) }}",
        "options": {
          "responseCode": 200
        }
      },
      "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, 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, 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, corpus: $('Parse Input').first().json.corpus } }) }}",
        "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\nconst input = $('Parse Input').first().json;\nconst chunkResult = $input.first().json;\n\nlet segments = [];\n\n// Check if chunking was done\nif (chunkResult.segments && Array.isArray(chunkResult.segments)) {\n  // Chunked segments from Chunk Worker\n  segments = chunkResult.segments.map((s, idx) => ({\n    index: idx,\n    text: s.text,\n    segment_id: input.segments[0]?.segment_id,\n    source_text: input.segments[0]?.source_text,\n    char_count: s.text?.length || 0,\n    isChunk: true,\n    totalChunks: chunkResult.segments.length\n  }));\n} else {\n  // Use original segments (no chunking needed or chunking failed)\n  segments = input.segments;\n}\n\nconst items = segments.map(segment => ({\n  json: {\n    ...segment,\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    model: input.model,\n    provider: input.provider,    \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    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    model: input.model,\n    provider: input.provider,    \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, text: $json.text, reference_translation: $json.reference_translation, source_language: $json.sourceLanguage, target_language: $json.targetLanguage, api_key: $json.apiKey, model: $json.model, 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": "strict",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.success }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "id": "e2895308-6839-44fc-b069-06497f701c58"
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "translate-success",
      "name": "Translate Success?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2864,
        304
      ]
    },
    {
      "parameters": {
        "jsCode": "// Phase 2: Prepare save payload(s) - handle EN pivot generation\nconst loopItem = $('Loop Segments').first().json;\nconst translateResult = $input.first().json;\n\n// Base payload for target language translation\nconst targetPayload = {\n  jobId: loopItem.jobId,\n  segmentIndex: loopItem.index,\n  totalSegments: loopItem.totalSegments,\n  translation: translateResult.translation,\n  segment_id: loopItem.segment_id,\n  commentary_id: loopItem.commentary_id,\n  source_text: loopItem.source_text,\n  targetLanguage: loopItem.targetLanguage,\n  model: loopItem.model,\n  provider: loopItem.provider,\n  metadata: loopItem.metadata,\n  isChunk: loopItem.isChunk || false,\n  pipelineType: loopItem.pipelineType,\n  method: translateResult.method || 'unknown'\n};\n\n// Phase 2: If EN was generated, we need to save it first\nconst hasGeneratedEnglish = translateResult.generated_english === true && translateResult.english_pivot;\n\nif (hasGeneratedEnglish) {\n  // Return TWO items: first EN save, then target language save\n  const englishPayload = {\n    jobId: loopItem.jobId,\n    segmentIndex: loopItem.index,\n    totalSegments: loopItem.totalSegments,\n    translation: translateResult.english_pivot,\n    segment_id: loopItem.segment_id,\n    commentary_id: loopItem.commentary_id,\n    source_text: loopItem.source_text,\n    targetLanguage: 'en',\n    model: loopItem.model,\n    provider: loopItem.provider,\n    metadata: { ...loopItem.metadata, is_generated_pivot: true },\n    isChunk: false,\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\n// No EN generation - just save target translation\nreturn [{ json: targetPayload }];"
      },
      "id": "prepare-save",
      "name": "Prepare Save",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3088,
        96
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.N8N_WEBHOOK_URL || 'http://localhost:5678' }}/webhook/torah-save",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n    segment_id: $json.segment_id,\n    commentary_id: $json.commentary_id,\n    source_text: $json.source_text,\n    translated_text: $json.translation,\n    target_language: $json.targetLanguage,\n    provider: $json.provider || 'anthropic',\n    model: $json.model || 'claude-sonnet-4',\n    quality_score: $json.qualityScore,\n    job_id: $json.jobId,\n    request_id: $json.requestId,\n    status: 'approved'\n  }) }}",
        "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: { 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": "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",
    "availableInMCP": false
  },
  "tags": [],
  "activeVersion": {
    "updatedAt": "2026-06-19T13:20:02.173Z",
    "createdAt": "2026-06-19T13:20:00.319Z",
    "versionId": "e382ed84-71b9-4c00-8a62-461e33dc7e51",
    "workflowId": "cJy6yP5bhwCq0o15",
    "nodes": [
      {
        "parameters": {
          "content": "## Torah Router\n\n**Endpoint:** `POST /webhook/torah-router`\n\n**Responsabilite:**\n- Analyse le payload\n- Decide du pipeline (chunk/translate/save)\n- Orchestre les workers\n- Met a jour le progress\n- Gere les erreurs\n\n**Pipelines:**\n- Batch commentaires: Translate -> Save (pour chaque)\n- Texte long: Chunk -> Translate -> Save (combine)\n- Texte court: Translate -> Save",
          "height": 340,
          "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
        ],
        "webhookId": "torah-router"
      },
      {
        "parameters": {
          "jsCode": "// Parse and validate input\nconst input = $input.first().json;\nconst body = input.body || input;\nconst headers = input.headers || {};\n\nconst CHUNK_THRESHOLD = 10000;\n\n// Generate job_id if not provided\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';\n\n// Determine pipeline type\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 (commentaries)\n  pipelineType = 'batch';\n  segments = body.segments.map((s, idx) => ({\n    index: idx,\n    text: typeof s === 'string' ? s : (s.text || s.text_hebrew || s.hebrew_text),\n    segment_id: s.segment_id || null,\n    commentary_id: s.commentary_id || null,\n    source_text: typeof s === 'string' ? s : null,\n    // RFC bilingual-translations-en-pivot: EN pivot for quality\n    reference_translation: s.reference_translation || null,\n    char_count: (typeof s === 'string' ? s : (s.text || s.text_hebrew || s.hebrew_text || '')).length\n  }));\n  \n  // Check if any segment is too long\n  const tooLongSegment = segments.find(s => s.char_count > CHUNK_THRESHOLD);\n  if (tooLongSegment) {\n    return [{\n      json: {\n        valid: false,\n        error: {\n          code: 400,\n          message: `Segment ${tooLongSegment.index} is too long (${tooLongSegment.char_count} chars). Max: ${CHUNK_THRESHOLD}. Use chunking for long texts.`\n        }\n      }\n    }];\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    // Segments will be created by Chunk Worker\n    segments = [{ index: 0, text: body.text, segment_id: body.segment_id || null, source_text: body.text, char_count: textLength }];\n    totalSegments = 1; // Will be updated after chunking\n  } else {\n    pipelineType = 'translate_single';\n    segments = [{\n      index: 0,\n      text: body.text,\n      segment_id: body.segment_id || null,\n      source_text: body.text,\n      char_count: textLength\n    }];\n    totalSegments = 1;\n  }\n} else {\n  return [{\n    json: {\n      valid: false,\n      error: { code: 400, message: 'Either segments[] or text is required' }\n    }\n  }];\n}\n\nreturn [{\n  json: {\n    valid: true,\n    jobId: jobId,\n    jobIdProvided: jobIdProvided,\n    needsJobCreation: !jobIdProvided,\n    projectId: projectId,\n    jobType: body.job_type || 'translation',\n    pipelineType: pipelineType,\n    needsChunking: needsChunking,\n    segments: segments,\n    totalSegments: totalSegments,\n    traite: body.traite,\n    page: body.page,\n    section: body.section,\n    commentator: body.commentator,\n    sourceLanguage: body.source_language || 'he',\n    targetLanguage: body.target_language || 'fr',\n    apiKey: body.api_key,\n    openaiApiKey: body.openai_api_key,\n    context: body.context || {},\n    metadata: body.metadata || {},\n    requestId: body.request_id,\n    model: body.model || 'claude-sonnet-4-6',\n    provider: body.provider || 'anthropic',\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": {
          "respondWith": "json",
          "responseBody": "={{ JSON.stringify({ success: false, error: $json.error }) }}",
          "options": {
            "responseCode": "={{ $json.error?.code || 400 }}"
          }
        },
        "id": "respond-error",
        "name": "Respond Error",
        "type": "n8n-nodes-base.respondToWebhook",
        "typeVersion": 1.1,
        "position": [
          1104,
          512
        ]
      },
      {
        "parameters": {
          "respondWith": "json",
          "responseBody": "={{ JSON.stringify({ received: true, job_id: $json.jobId, pipeline: $json.pipelineType, segments_count: $json.totalSegments }) }}",
          "options": {
            "responseCode": 200
          }
        },
        "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, 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, 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, corpus: $('Parse Input').first().json.corpus } }) }}",
          "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\nconst input = $('Parse Input').first().json;\nconst chunkResult = $input.first().json;\n\nlet segments = [];\n\n// Check if chunking was done\nif (chunkResult.segments && Array.isArray(chunkResult.segments)) {\n  // Chunked segments from Chunk Worker\n  segments = chunkResult.segments.map((s, idx) => ({\n    index: idx,\n    text: s.text,\n    segment_id: input.segments[0]?.segment_id,\n    source_text: input.segments[0]?.source_text,\n    char_count: s.text?.length || 0,\n    isChunk: true,\n    totalChunks: chunkResult.segments.length\n  }));\n} else {\n  // Use original segments (no chunking needed or chunking failed)\n  segments = input.segments;\n}\n\nconst items = segments.map(segment => ({\n  json: {\n    ...segment,\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    model: input.model,\n    provider: input.provider,    \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    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    model: input.model,\n    provider: input.provider,    \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, text: $json.text, reference_translation: $json.reference_translation, source_language: $json.sourceLanguage, target_language: $json.targetLanguage, api_key: $json.apiKey, model: $json.model, 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": "strict",
              "version": 2
            },
            "conditions": [
              {
                "leftValue": "={{ $json.success }}",
                "rightValue": true,
                "operator": {
                  "type": "boolean",
                  "operation": "equals"
                },
                "id": "e2895308-6839-44fc-b069-06497f701c58"
              }
            ],
            "combinator": "and"
          },
          "options": {}
        },
        "id": "translate-success",
        "name": "Translate Success?",
        "type": "n8n-nodes-base.if",
        "typeVersion": 2.2,
        "position": [
          2864,
          304
        ]
      },
      {
        "parameters": {
          "jsCode": "// Phase 2: Prepare save payload(s) - handle EN pivot generation\nconst loopItem = $('Loop Segments').first().json;\nconst translateResult = $input.first().json;\n\n// Base payload for target language translation\nconst targetPayload = {\n  jobId: loopItem.jobId,\n  segmentIndex: loopItem.index,\n  totalSegments: loopItem.totalSegments,\n  translation: translateResult.translation,\n  segment_id: loopItem.segment_id,\n  commentary_id: loopItem.commentary_id,\n  source_text: loopItem.source_text,\n  targetLanguage: loopItem.targetLanguage,\n  model: loopItem.model,\n  provider: loopItem.provider,\n  metadata: loopItem.metadata,\n  isChunk: loopItem.isChunk || false,\n  pipelineType: loopItem.pipelineType,\n  method: translateResult.method || 'unknown'\n};\n\n// Phase 2: If EN was generated, we need to save it first\nconst hasGeneratedEnglish = translateResult.generated_english === true && translateResult.english_pivot;\n\nif (hasGeneratedEnglish) {\n  // Return TWO items: first EN save, then target language save\n  const englishPayload = {\n    jobId: loopItem.jobId,\n    segmentIndex: loopItem.index,\n    totalSegments: loopItem.totalSegments,\n    translation: translateResult.english_pivot,\n    segment_id: loopItem.segment_id,\n    commentary_id: loopItem.commentary_id,\n    source_text: loopItem.source_text,\n    targetLanguage: 'en',\n    model: loopItem.model,\n    provider: loopItem.provider,\n    metadata: { ...loopItem.metadata, is_generated_pivot: true },\n    isChunk: false,\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\n// No EN generation - just save target translation\nreturn [{ json: targetPayload }];"
        },
        "id": "prepare-save",
        "name": "Prepare Save",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          3088,
          96
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ $env.N8N_WEBHOOK_URL || 'http://localhost:5678' }}/webhook/torah-save",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({\n    segment_id: $json.segment_id,\n    commentary_id: $json.commentary_id,\n    source_text: $json.source_text,\n    translated_text: $json.translation,\n    target_language: $json.targetLanguage,\n    provider: $json.provider || 'anthropic',\n    model: $json.model || 'claude-sonnet-4',\n    quality_score: $json.qualityScore,\n    job_id: $json.jobId,\n    request_id: $json.requestId,\n    status: 'approved'\n  }) }}",
          "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: { 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": "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
            }
          ]
        ]
      }
    },
    "authors": "Sebbah fsebbah@azy.solutions",
    "name": "Version e382ed84",
    "description": "",
    "autosaved": true,
    "workflowPublishHistory": [
      {
        "createdAt": "2026-06-19T13:20:02.164Z",
        "id": 158,
        "workflowId": "cJy6yP5bhwCq0o15",
        "versionId": "e382ed84-71b9-4c00-8a62-461e33dc7e51",
        "event": "activated",
        "userId": "af142d6e-919f-4345-b234-759caa50a7bb"
      }
    ]
  }
}
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

eek-Go v2 (Batch-Then-Review). Uses httpRequest. Webhook trigger; 75 nodes.

HTTP Request
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