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 →
{
"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"
}
]
}
}
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
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
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .
eek-Go v2 (Batch-Then-Review). Uses httpRequest. Webhook trigger; 75 nodes.
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
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