This workflow follows the Emailsend → OpenAI recipe pattern — see all workflows that pair these two integrations.
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 →
{
"updatedAt": "2025-09-23T08:27:24.000Z",
"createdAt": "2025-08-11T18:02:34.196Z",
"id": "JGVj7vwvk7WvsGvP",
"name": "My workflow",
"active": false,
"isArchived": false,
"nodes": [
{
"parameters": {},
"name": "Start",
"type": "n8n-nodes-base.start",
"typeVersion": 1,
"position": [
-1600,
304
],
"id": "1611cbdf-5641-4f3e-9c0f-7cfabb7665b0"
},
{
"parameters": {
"httpMethod": "POST",
"path": "pyragogy/process",
"options": {}
},
"name": "Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-1408,
304
],
"id": "5dd5fcb3-5968-4669-b279-cb7db2d5d02b"
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT inet_server_addr(); Verifica connessione DB",
"additionalFields": {}
},
"name": "Check DB Connection",
"type": "n8n-nodes-base.postgres",
"typeVersion": 1,
"position": [
-1200,
304
],
"id": "c2a4998b-776c-426f-b6f9-b0809e615849",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"resource": "chat",
"model": "gpt-4o",
"options": {},
"requestOptions": {}
},
"name": "Meta-Orchestrator",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [
-1008,
304
],
"id": "731e5488-9ec8-42e5-8b47-f43fa8d2e39e",
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"functionCode": "// Analizza il piano di orchestrazione e imposta per il looping\nlet rawPlan = $json.choices[0].message.content;\nlet plan;\n\ntry {\n plan = JSON.parse(rawPlan);\n} catch (e) {\n // Se il parsing fallisce, assumi che sia una stringa di array grezza\n plan = rawPlan;\n}\n\n// Estrai in modo sicuro la sequenza degli agenti:\n// Se 'plan' \u00e8 un oggetto e ha una chiave 'agents', usala.\n// Altrimenti, se 'plan' \u00e8 un array, usalo direttamente.\n// Altrimenti, predefinisci un array vuoto.\nconst agentSequence = Array.isArray(plan) ? plan : (plan && plan.agents && Array.isArray(plan.agents) ? plan.agents : []);\n\n// Memorizza la sequenza e l'indice corrente per il loop\n$workflow.agentSequence = agentSequence;\n$workflow.currentAgentIndex = 0;\n$workflow.redraftLoopCount = 0; // Inizializza il contatore dei cicli di rielaborazione\n\n// Passa i dati di input al primo agente, assicurandosi che $items[0].json.body esista\nconst initialInput = $items[0] && $items[0].json && $items[0].json.body ? $items[0].json.body : {};\n\nreturn [{ json: { ...initialInput, agentToRun: agentSequence[0] || null } }];"
},
"name": "Parse Orchestration Plan",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
-800,
304
],
"id": "c7559cb4-9ec4-41b1-8d9f-fe414c61e558"
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $workflow.currentAgentIndex < $workflow.agentSequence.length }}",
"value2": true
}
]
}
},
"name": "More Agents to Run?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-608,
304
],
"id": "aa433d5d-ef0c-42ae-b63f-b707fa594702"
},
{
"parameters": {
"functionCode": "// Ottieni il nome dell'agente corrente\nconst agentName = $workflow.agentSequence[$workflow.currentAgentIndex];\n\n// Prepara i dati per l'esecuzione dell'agente\n// Passa l'output del passo precedente (o l'input iniziale).\n// Se stiamo rielaborando, l'input dell'agente dovrebbe includere il feedback di rielaborazione.\nconst previousOutput = $json.output || $json.body.input; // Assumendo che l'output dell'agente sia memorizzato nella chiave 'output'\nconst agentInput = $json.redraftInput || previousOutput; // Usa redraftInput se presente, altrimenti previousOutput\n\nreturn [{ json: { ...$json, agentToRun: agentName, agentInput: agentInput } }];"
},
"name": "Prepare Agent Input",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
-400,
208
],
"id": "9e7b117a-9a2a-4f49-9e78-66e8bd145a1d"
},
{
"parameters": {
"mode": "json"
},
"name": "Route Agents with Switch",
"type": "n8n-nodes-base.switch",
"typeVersion": 1,
"position": [
-208,
208
],
"id": "3181be02-2e59-483e-b5e5-9e7561d8e082"
},
{
"parameters": {
"resource": "chat",
"model": "gpt-4o",
"options": {},
"requestOptions": {}
},
"name": "Summarizer Agent",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [
0,
-32
],
"id": "a8e44d92-da29-4613-a1ed-7a6301c5ec1b",
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"resource": "chat",
"model": "gpt-4o",
"options": {},
"requestOptions": {}
},
"name": "Synthesizer Agent",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [
0,
112
],
"id": "467c5542-5f2c-4589-b707-fe455177d0e6",
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"resource": "chat",
"model": "gpt-4o",
"options": {},
"requestOptions": {}
},
"name": "Peer Reviewer Agent",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [
0,
208
],
"id": "a9b2d78d-1e6c-4256-ad52-98dd21cffd0b",
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"resource": "chat",
"model": "gpt-4o",
"options": {},
"requestOptions": {}
},
"name": "Sensemaking Agent",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [
0,
304
],
"id": "a9d0bc05-0b5b-403f-962c-29daa0618049",
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"resource": "chat",
"model": "gpt-4o",
"options": {},
"requestOptions": {}
},
"name": "Prompt Engineer Agent",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [
0,
400
],
"id": "1c32717e-1807-483d-9c15-3119149dae03",
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"resource": "chat",
"model": "gpt-4o",
"options": {},
"requestOptions": {}
},
"name": "Onboarding/Explainer Agent",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [
0,
512
],
"id": "2550f8d1-24e3-4384-9cd1-d09d82f9f230",
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"functionCode": "// Prepara i metadati per il contenuto dell'Handbook.\n// Assicurati che l'input originale contenga 'title' e 'tags' o imposta dei valori predefiniti.\nconst title = $json.body.title || 'Untitled Handbook Entry';\nconst tags = $json.body.tags || [];\nconst phase = $json.body.phase || 'draft'; // Fase iniziale, pu\u00f2 essere 'final' dopo l'approvazione\nconst rhythm = $json.body.rhythm || 'on-demand'; // Ritmo cognitivo\n\nreturn [{ json: { ...$json, handbookTitle: title, handbookTags: tags, handbookPhase: phase, handbookRhythm: rhythm } }];"
},
"name": "Add Handbook Metadata",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
0,
608
],
"id": "477a28c8-e9b2-4da2-ae40-8adf45785b97"
},
{
"parameters": {
"functionCode": "// Prepara il contenuto proposto dall'Archivista per la revisione umana, inclusa la formattazione YAML.\n// Assicurati che l'input dell'agente (il contenuto generato) sia disponibile.\nconst proposedContent = $json.agentInput;\nconst title = $json.handbookTitle;\nconst tags = $json.handbookTags;\nconst phase = $json.handbookPhase;\nconst rhythm = $json.handbookRhythm;\n\n// Costruisci il front-matter YAML\nconst yamlFrontMatter = `---\ntitle: \"${title.replace(/\"/g, '\\\"')}\"\ntags: [${tags.map(t => `\"${t.replace(/\"/g, '\\\"')}\"`).join(', ')}]\nphase: \"${phase}\"\nrhythm: \"${rhythm}\"\n---\n\n`;\n\nconst finalMarkdownContent = yamlFrontMatter + proposedContent;\n\nreturn [{ json: { ...$json, proposedContent: proposedContent, reviewTitle: title, reviewTags: tags, finalMarkdownContent: finalMarkdownContent } }];"
},
"name": "Generate Content for Review",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
208,
608
],
"id": "0b5c46ad-843d-4905-8d8e-e9dd762ebf8b"
},
{
"parameters": {
"functionCode": "// Genera un ID univoco per questa richiesta di revisione.\n// Questo ID verr\u00e0 usato per correlare la risposta del revisore con questa istanza del workflow.\nconst reviewId = crypto.randomUUID();\n\nreturn [{ json: { ...$json, reviewId: reviewId } }];"
},
"name": "Generate Review ID",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
400,
608
],
"id": "33c279cc-e085-402d-a951-aba94cbd9416"
},
{
"parameters": {
"fromEmail": "your-email@example.com",
"toEmail": "human-reviewer@example.com",
"subject": "Revisione Contenuto Pyragogy Handbook: {{ $json.reviewTitle }}",
"text": "Ciao revisore,\n\n\u00c8 stato proposto un nuovo contenuto per l'Handbook:\n\n---\n{{ $json.proposedContent }}\n---\n\nTitolo: {{ $json.reviewTitle }}\nTags: {{ $json.reviewTags.join(', ') }}\n\nPer favore, clicca su uno dei seguenti link per approvare o rifiutare:\n\nApprova: your_n8n_url/webhook/pyragogy/review-feedback?reviewId={{ $json.reviewId }}&status=approved\nRifiuta: your_n8n_url/webhook/pyragogy/review-feedback?reviewId={{ $json.reviewId }}&status=rejected\n\nGrazie!",
"html": "<h3>Revisione Contenuto Pyragogy Handbook: {{ $json.reviewTitle }}</h3>\n<p>Ciao revisore,</p>\n<p>\u00c8 stato proposto un nuovo contenuto per l'Handbook:</p>\n<hr>\n<pre>{{ $json.proposedContent }}</pre>\n<hr>\n<p><strong>Titolo:</strong> {{ $json.reviewTitle }}</p>\n<p><strong>Tags:</strong> {{ $json.reviewTags.join(', ') }}</p>\n<p>Per favore, clicca su uno dei seguenti link per approvare o rifiutare:</p>\n<p><a href=\"your_n8n_url/webhook/pyragogy/review-feedback?reviewId={{ $json.reviewId }}&status=approved\">Approva</a></p>\n<p><a href=\"your_n8n_url/webhook/pyragogy/review-feedback?reviewId={{ $json.reviewId }}&status=rejected\">Rifiuta</a></p>\n<p>Grazie!</p>",
"options": {}
},
"name": "Send Review Request Email",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 1,
"position": [
608,
608
],
"id": "96fbe885-670b-49c4-b724-e44b42ab3451"
},
{
"parameters": {},
"name": "Wait for Human Approval",
"type": "n8n-nodes-base.wait",
"typeVersion": 1,
"position": [
800,
608
],
"id": "8c5b668e-e05e-41bd-bd46-fe4a2f7d74b8"
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.query.status === 'approved' }}",
"value2": true
}
]
}
},
"name": "Human Decision Split",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
1008,
608
],
"id": "8195c68f-cb67-439f-8b7b-e883c39dcb3a"
},
{
"parameters": {
"table": "handbook_entries",
"columns": "title, content, version, created_by, tags, phase, rhythm",
"additionalFields": {}
},
"name": "Save to handbook_entries",
"type": "n8n-nodes-base.postgres",
"typeVersion": 1,
"position": [
1200,
512
],
"id": "94231ed6-1af0-4329-b584-879b82447f3e"
},
{
"parameters": {
"functionCode": "// Registra il contributo dell'agente dopo l'approvazione umana\nconst entryId = $json.id; // ID dall'inserimento in handbook_entries\nconst agentName = 'Archivist';\nconst contributionType = 'Archiving (Approved)';\nconst details = { input: $json.proposedContent, metadata: { title: $json.reviewTitle, version: $json.version, tags: $json.reviewTags, phase: $json.handbookPhase, rhythm: $json.handbookRhythm }, reviewStatus: 'approved' };\n\n$items[0].json.contribution = { entryId, agentName, contributionType, details };\nreturn $items;"
},
"name": "Prepare Approved Contribution Data",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
1408,
512
],
"id": "0e41cbca-d0ec-45fc-b6c9-5357057bca48"
},
{
"parameters": {
"table": "agent_contributions",
"columns": "entry_id, agent_name, contribution_type, details",
"additionalFields": {}
},
"name": "Save Agent Contribution (Approved)",
"type": "n8n-nodes-base.postgres",
"typeVersion": 1,
"position": [
1600,
512
],
"id": "0e7ff61f-e439-4225-9538-84be6a0e48fc"
},
{
"parameters": {
"functionCode": "// Genera il percorso del file GitHub con slug e timestamp per il versioning.\nconst chapterSlug = ($json.reviewTitle || 'untitled').replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();\nconst timestamp = new Date().toISOString().replace(/[:.-]/g, ''); // Rimuove caratteri non validi per i nomi di file\nconst filePathWithVersion = `content/${chapterSlug}_v${timestamp}.md`;\n\nreturn [{ json: { ...$json, githubFilePath: filePathWithVersion } }];"
},
"name": "Generate GitHub File Path",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
1808,
512
],
"id": "eb6ae210-b74d-4c64-aec5-641955c6951a"
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $env.GITHUB_ACCESS_TOKEN }}",
"value2": true
}
]
}
},
"name": "GitHub Enabled?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
2000,
512
],
"id": "37ef7cd3-eda2-49da-a533-5b4e28560c96"
},
{
"parameters": {
"resource": "file",
"operation": "createUpdate",
"owner": {
"__rl": true,
"value": "={{ $env.GITHUB_REPOSITORY_OWNER }}"
},
"repository": "={{ $env.GITHUB_REPOSITORY_NAME }}",
"filePath": "={{ $json.githubFilePath }}"
},
"name": "Commit to GitHub (Approved)",
"type": "n8n-nodes-base.github",
"typeVersion": 1,
"position": [
2208,
512
],
"id": "a534edf4-f888-455c-979e-6f84f99fcd3d"
},
{
"parameters": {
"functionCode": "// Registra il rifiuto umano del contenuto\nconst agentName = 'Archivist';\nconst reviewId = $json.reviewId;\nconst reviewStatus = 'rejected';\nconst reviewComments = $json.query.comments || 'Nessun commento fornito.';\nconst proposedContent = $json.proposedContent;\nconst title = $json.reviewTitle;\n\nconsole.log(`Contenuto proposto dall'Archivista (ID: ${reviewId}, Titolo: ${title}) rifiutato dall'umano. Commenti: ${reviewComments}`);\n\n// Prepara l'output per indicare il rifiuto e consentire al flusso di continuare.\nreturn [{ json: { ...$json, reviewStatus: reviewStatus, reviewComments: reviewComments, output: { message: `Archivista: Contenuto rifiutato dall'umano.`, status: 'rejected', comments: reviewComments } } }];"
},
"name": "Log Human Rejection",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
1200,
704
],
"id": "c1130f1a-cdc6-42c6-999f-34f2ceeeeeee"
},
{
"parameters": {
"mode": "mergeByPropertyName"
},
"name": "Merge Archivist Paths",
"type": "n8n-nodes-base.merge",
"typeVersion": 1,
"position": [
2400,
608
],
"id": "4ffbe6ee-b42b-41a0-858e-cfb0861ddd26"
},
{
"parameters": {
"functionCode": "// Valuta i flag 'major_issue' dagli agenti di revisione per determinare se \u00e8 necessaria una rielaborazione.\nlet majorIssueCount = 0;\nlet redraftFeedback = '';\n\n// Assumi che gli output degli agenti di revisione siano accessibili tramite $node\n// (Ad esempio, se Peer Reviewer -> Sensemaking -> Prompt Engineer sono sequenziali prima di questo nodo)\n\nif ($node[\"Peer Reviewer Agent\"] && $node[\"Peer Reviewer Agent\"].json && $node[\"Peer Reviewer Agent\"].json.choices && $node[\"Peer Reviewer Agent\"].json.choices[0] && $node[\"Peer Reviewer Agent\"].json.choices[0].message && $node[\"Peer Reviewer Agent\"].json.choices[0].message.content) {\n const peerReviewOutput = JSON.parse($node[\"Peer Reviewer Agent\"].json.choices[0].message.content);\n if (peerReviewOutput.major_issue) majorIssueCount++;\n redraftFeedback += `Peer Reviewer: ${peerReviewOutput.suggestions || ''}\\n`;\n}\n\nif ($node[\"Sensemaking Agent\"] && $node[\"Sensemaking Agent\"].json && $node[\"Sensemaking Agent\"].json.choices && $node[\"Sensemaking Agent\"].json.choices[0] && $node[\"Sensemaking Agent\"].json.choices[0].message && $node[\"Sensemaking Agent\"].json.choices[0].message.content) {\n const sensemakingOutput = JSON.parse($node[\"Sensemaking Agent\"].json.choices[0].message.content);\n if (sensemakingOutput.major_issue) majorIssueCount++;\n redraftFeedback += `Sensemaking Agent: ${sensemakingOutput.suggestions || ''}\\n`;\n}\n\nif ($node[\"Prompt Engineer Agent\"] && $node[\"Prompt Engineer Agent\"].json && $node[\"Prompt Engineer Agent\"].json.choices && $node[\"Prompt Engineer Agent\"].json.choices[0] && $node[\"Prompt Engineer Agent\"].json.choices[0].message && $node[\"Prompt Engineer Agent\"].json.choices[0].message.content) {\n const promptEngineerOutput = JSON.parse($node[\"Prompt Engineer Agent\"].json.choices[0].message.content);\n if (promptEngineerOutput.major_issue) majorIssueCount++;\n redraftFeedback += `Prompt Engineer: ${promptEngineerOutput.suggestions || ''}\\n`;\n}\n\nconst redraftNeeded = majorIssueCount >= 2; // Voto a maggioranza\n\nreturn [{ json: { ...$json, redraftNeeded: redraftNeeded, redraftFeedback: redraftFeedback } }];"
},
"name": "Evaluate Board Consensus",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
208,
304
],
"id": "f72b6317-8321-4413-9a05-4369a968bc06"
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.redraftNeeded && $workflow.redraftLoopCount < 2 }}",
"value2": true
}
]
}
},
"name": "Check Redraft Needed",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
400,
304
],
"id": "c050efd6-78b0-4bce-9289-2786011a4343"
},
{
"parameters": {
"functionCode": "// Gestisce la logica di rielaborazione: incrementa il contatore e reindirizza al Synthesizer.\n\n$workflow.redraftLoopCount += 1; // Incrementa il contatore del ciclo di rielaborazione\n\n// Trova l'indice del Synthesizer nella sequenza degli agenti\nconst synthesizerIndex = $workflow.agentSequence.indexOf(\"Synthesizer\");\n\n// Se il Synthesizer non \u00e8 il prossimo agente, imposta l'indice corrente per farlo ripartire dal Synthesizer.\n// Questo garantisce che il Synthesizer venga eseguito successivamente per la rielaborazione.\nif ($workflow.currentAgentIndex !== synthesizerIndex) {\n $workflow.currentAgentIndex = synthesizerIndex;\n} else {\n // Se siamo gi\u00e0 sul Synthesizer (es. dopo il primo passaggio del loop), assicurati che l'indice vada avanti normalmente nel prossimo ciclo.\n // Questo \u00e8 un caso limite, di solito il Prepare Agent Input lo gestir\u00e0.\n}\n\n// Passa il feedback di rielaborazione come input per il Synthesizer.\n// Il nodo 'Prepare Agent Input' utilizzer\u00e0 questo campo per aggiornare 'agentInput'.\nreturn [{ json: { ...$json, redraftInput: $json.output + \"\\n\\nFeedback per la rielaborazione dal Peer Review Board:\\n\" + $json.redraftFeedback } }];"
},
"name": "Handle Redraft",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
608,
208
],
"id": "a47ac601-1a85-402b-8df2-8587a635d8e5"
},
{
"parameters": {
"functionCode": "// Ottieni il nome dell'agente appena eseguito\nconst agentName = $json.agentToRun;\nlet agentOutput = '';\n\n// Costruisci dinamicamente il nome del nodo per il recupero dell'output\nlet actualNodeName;\n\n// Gestione speciale per l'Archivista dopo la revisione umana\nif (agentName === 'Archivist') {\n if ($json.query && $json.query.status === 'approved') {\n agentOutput = { message: 'Contenuto archiviato con successo dopo l'approvazione umana.', entryId: $json.id || 'N/A', handbookMetadata: { title: $json.reviewTitle, tags: $json.reviewTags, phase: $json.handbookPhase, rhythm: $json.handbookRhythm } };\n } else if ($json.query && $json.query.status === 'rejected') {\n agentOutput = { message: 'Archiviazione rifiutata dall'umano.', reviewComments: $json.query.comments || 'Nessun commento' };\n } else if ($json.reviewStatus === 'rejected') {\n // Caso in cui la revisione umana \u00e8 stata rifiutata e si proviene dal ramo di rifiuto\n agentOutput = { message: 'Archiviazione rifiutata dall'umano (tramite ramo di rifiuto).', reviewComments: $json.reviewComments || 'Nessun commento' };\n } else {\n agentOutput = { message: 'Processo Archivista: Attesa revisione umana o stato inatteso.', status: 'pending_review' };\n }\n} else {\n // Logica originale per gli altri agenti OpenAI\n if (agentName === 'Onboarding/Explainer') {\n actualNodeName = 'Onboarding/Explainer Agent';\n } else {\n actualNodeName = agentName + ' Agent';\n }\n\n // Tenta in modo sicuro di recuperare l'output dal nodo determinato dinamicamente\n // Tenta anche di analizzare il JSON se l'output dell'agente \u00e8 un oggetto JSON (come per gli agenti di revisione)\n let rawContent = '';\n if ($node[actualNodeName] && $node[actualNodeName].json && $node[actualNodeName].json.choices && $node[actualNodeName].json.choices[0] && $node[actualNodeName].json.choices[0].message && $node[actualNodeName].json.choices[0].message.content) {\n rawContent = $node[actualNodeName].json.choices[0].message.content;\n } else {\n console.warn(`Impossibile trovare l'output chat OpenAI standard per il nodo: ${actualNodeName}. Ritorno al JSON grezzo.`);\n agentOutput = $node[actualNodeName] ? $node[actualNodeName].json : `Nessun output specifico trovato per ${agentName}`;\n }\n\n // Tenta di analizzare il contenuto come JSON se l'agente \u00e8 un agente di revisione\n if (agentName === 'Peer Reviewer' || agentName === 'Sensemaking Agent' || agentName === 'Prompt Engineer') {\n try {\n agentOutput = JSON.parse(rawContent);\n } catch (e) {\n console.warn(`Impossibile analizzare l'output di ${agentName} come JSON. Trattato come stringa.`);\n agentOutput = rawContent;\n }\n } else {\n agentOutput = rawContent;\n }\n}\n\n// Registra il contributo\nconst contribution = {\n agent: agentName,\n output: agentOutput,\n timestamp: new Date().toISOString()\n};\n\n// Assicurati che l'array dei contributi esista e aggiungi il nuovo contributo\nconst existingContributions = Array.isArray($json.contributions) ? $json.contributions : [];\n\n// Incrementa l'indice dell'agente per il loop (solo se non siamo in rielaborazione e questo non \u00e8 un agente di revisione che porta a rielaborazione)\n// Questo \u00e8 gestito dalla logica di 'Handle Redraft' che forza l'indice per il riavvio.\nif (!($json.redraftNeeded && $workflow.redraftLoopCount < 2 && agentName !== 'Synthesizer')) { // Evita doppio incremento se riavvia il Synthesizer\n $workflow.currentAgentIndex += 1;\n}\n\n// Restituisce l'elemento aggiornato, preservando l'input originale, aggiungendo l'output corrente e tutti i contributi\nreturn [{ json: { ...$items[0].json, output: agentOutput, contributions: [...existingContributions, contribution] } }];"
},
"name": "Process Agent Output",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
400,
208
],
"id": "b7db08ff-4676-4a69-8b59-0f24788838f1"
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $env.SLACK_WEBHOOK_URL }}",
"value2": true
}
]
}
},
"name": "Slack Enabled?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-400,
512
],
"id": "e34f26d8-2fa6-462c-b103-6c6625935806"
},
{
"parameters": {
"text": "Pyragogy AI Village Workflow Completato!\nInput: {{ $json.body.input }}\nOutput Finale: {{ JSON.stringify($json.output) }}\nAgenti Eseguiti: {{ $workflow.agentSequence.join(', ') }}",
"otherOptions": {},
"attachments": []
},
"name": "Notify Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 1,
"position": [
-208,
512
],
"id": "0023b925-7e02-43d9-83bd-9a9ebee63cb8"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { finalOutput: $json.output, contributions: $json.contributions, agentSequence: $workflow.agentSequence } }}",
"options": {}
},
"name": "Final Response",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
-400,
400
],
"id": "c04667b0-8fff-4fa4-8fc6-c9a106a4d687"
}
],
"connections": {
"Webhook Trigger": {
"main": [
[
{
"node": "Check DB Connection",
"type": "main",
"index": 0
}
]
]
},
"Check DB Connection": {
"main": [
[
{
"node": "Meta-Orchestrator",
"type": "main",
"index": 0
}
]
]
},
"Meta-Orchestrator": {
"main": [
[
{
"node": "Parse Orchestration Plan",
"type": "main",
"index": 0
}
]
]
},
"Parse Orchestration Plan": {
"main": [
[
{
"node": "More Agents to Run?",
"type": "main",
"index": 0
}
]
]
},
"More Agents to Run?": {
"main": [
[
{
"node": "Prepare Agent Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Slack Enabled?",
"type": "main",
"index": 0
}
]
]
},
"Prepare Agent Input": {
"main": [
[
{
"node": "Route Agents with Switch",
"type": "main",
"index": 0
}
]
]
},
"Route Agents with Switch": {
"main": [
[
{
"node": "Summarizer Agent",
"type": "main",
"index": 0
}
]
]
},
"Summarizer Agent": {
"main": [
[
{
"node": "Process Agent Output",
"type": "main",
"index": 0
}
]
]
},
"Synthesizer Agent": {
"main": [
[
{
"node": "Process Agent Output",
"type": "main",
"index": 0
}
]
]
},
"Peer Reviewer Agent": {
"main": [
[
{
"node": "Sensemaking Agent",
"type": "main",
"index": 0
}
]
]
},
"Sensemaking Agent": {
"main": [
[
{
"node": "Prompt Engineer Agent",
"type": "main",
"index": 0
}
]
]
},
"Prompt Engineer Agent": {
"main": [
[
{
"node": "Evaluate Board Consensus",
"type": "main",
"index": 0
}
]
]
},
"Onboarding/Explainer Agent": {
"main": [
[
{
"node": "Process Agent Output",
"type": "main",
"index": 0
}
]
]
},
"Add Handbook Metadata": {
"main": [
[
{
"node": "Generate Content for Review",
"type": "main",
"index": 0
}
]
]
},
"Generate Content for Review": {
"main": [
[
{
"node": "Generate Review ID",
"type": "main",
"index": 0
}
]
]
},
"Generate Review ID": {
"main": [
[
{
"node": "Send Review Request Email",
"type": "main",
"index": 0
}
]
]
},
"Send Review Request Email": {
"main": [
[
{
"node": "Wait for Human Approval",
"type": "main",
"index": 0
}
]
]
},
"Wait for Human Approval": {
"main": [
[
{
"node": "Human Decision Split",
"type": "main",
"index": 0
}
]
]
},
"Human Decision Split": {
"main": [
[
{
"node": "Save to handbook_entries",
"type": "main",
"index": 0
}
],
[
{
"node": "Log Human Rejection",
"type": "main",
"index": 0
}
]
]
},
"Save to handbook_entries": {
"main": [
[
{
"node": "Prepare Approved Contribution Data",
"type": "main",
"index": 0
}
]
]
},
"Prepare Approved Contribution Data": {
"main": [
[
{
"node": "Save Agent Contribution (Approved)",
"type": "main",
"index": 0
}
]
]
},
"Save Agent Contribution (Approved)": {
"main": [
[
{
"node": "Generate GitHub File Path",
"type": "main",
"index": 0
}
]
]
},
"Generate GitHub File Path": {
"main": [
[
{
"node": "GitHub Enabled?",
"type": "main",
"index": 0
}
]
]
},
"GitHub Enabled?": {
"main": [
[
{
"node": "Commit to GitHub (Approved)",
"type": "main",
"index": 0
}
],
[
{
"node": "Merge Archivist Paths",
"type": "main",
"index": 0
}
]
]
},
"Commit to GitHub (Approved)": {
"main": [
[
{
"node": "Merge Archivist Paths",
"type": "main",
"index": 0
}
]
]
},
"Log Human Rejection": {
"main": [
[
{
"node": "Merge Archivist Paths",
"type": "main",
"index": 0
}
]
]
},
"Merge Archivist Paths": {
"main": [
[
{
"node": "Process Agent Output",
"type": "main",
"index": 0
}
]
]
},
"Evaluate Board Consensus": {
"main": [
[
{
"node": "Check Redraft Needed",
"type": "main",
"index": 0
}
]
]
},
"Check Redraft Needed": {
"main": [
[
{
"node": "Handle Redraft",
"type": "main",
"index": 0
}
],
[
{
"node": "Process Agent Output",
"type": "main",
"index": 0
}
]
]
},
"Handle Redraft": {
"main": [
[
{
"node": "More Agents to Run?",
"type": "main",
"index": 0
}
]
]
},
"Process Agent Output": {
"main": [
[
{
"node": "More Agents to Run?",
"type": "main",
"index": 0
}
]
]
},
"Slack Enabled?": {
"main": [
[
{
"node": "Notify Slack",
"type": "main",
"index": 0
}
],
[
{
"node": "Final Response",
"type": "main",
"index": 0
}
]
]
},
"Notify Slack": {
"main": [
[
{
"node": "Final Response",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"meta": null,
"versionId": "6280c054-718c-455a-901f-760d5ecfcaf1",
"activeVersionId": null,
"triggerCount": 0,
"shared": [
{
"updatedAt": "2025-08-11T18:02:34.204Z",
"createdAt": "2025-08-11T18:02:34.204Z",
"role": "workflow:owner",
"workflowId": "JGVj7vwvk7WvsGvP",
"projectId": "HHopAZ4lOFgjhBzT"
}
],
"activeVersion": null,
"tags": [
{
"updatedAt": "2025-08-11T18:00:37.173Z",
"createdAt": "2025-08-11T18:00:37.173Z",
"id": "FrTwNLPlDK1Iz254",
"name": "Human-in-Loop"
},
{
"updatedAt": "2025-08-11T18:00:37.179Z",
"createdAt": "2025-08-11T18:00:37.179Z",
"id": "Rmm17sRhdbs3zRxU",
"name": "Orchestration"
},
{
"updatedAt": "2025-08-11T18:00:37.119Z",
"createdAt": "2025-08-11T18:00:37.119Z",
"id": "VpIJNM77qNRE1hqp",
"name": "Pyragogy"
},
{
"updatedAt": "2025-08-11T18:00:37.108Z",
"createdAt": "2025-08-11T18:00:37.108Z",
"id": "WhZ0EMlhJQIwh2aa",
"name": "Multi-Agent"
}
]
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
openAiApipostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
My-Workflow-Jgvj7Vwvk7Wvsgvp. Uses start, postgres, openAi, emailSend. Webhook trigger; 35 nodes.
Source: https://github.com/abde0112/n8n_bkv2/blob/46910d507b6d39c2daa31e4c5789bf9da9457116/my-workflow-JGVj7vwvk7WvsGvP.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.
Pyragogy AI Village - Orchestrazione Master (Architettura Profonda V2). Uses start, postgres, openAi, emailSend. Webhook trigger; 37 nodes.
Pyragogy AI Village - Orchestrazione Master (Architettura Profonda V2). Uses start, postgres, openAi, emailSend. Webhook trigger; 36 nodes.
Pyragogy AI Village - Orchestrazione Master (Architettura Profonda V2). Uses start, postgres, openAi, emailSend. Webhook trigger; 35 nodes.
Pyragogy AI Village - Orchestrazione Master (Architettura Profonda V2). Uses start, postgres, openAi, emailSend. Webhook trigger; 37 nodes.
AI-Driven Handbook Generator with Multi-Agent Orchestration (Pyragogy AI Village). Uses start, postgres, openAi, emailSend. Webhook trigger; 36 nodes.