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 →
{
"id": "eTfM9cjV0i7PQtsZ",
"meta": {
"site": "https://github.com/zengfr/n8n-workflow-all-templates",
"name": "Pyragogy AI-Driven Handbook Generator with Multi-Agent Orchestration",
"wechat": "youandme10086",
"id": 4904,
"update_time": "2025-11-10"
},
"name": "AI-Driven Handbook Generator with Multi-Agent Orchestration (Pyragogy AI Village)",
"tags": [
{
"id": "cPTJEblSALLWTPH5",
"name": "Pyragogy",
"createdAt": "2025-06-12T12:35:25.060Z",
"updatedAt": "2025-06-12T12:35:25.060Z"
},
{
"id": "6QE2GMT2wg7PfNjo",
"name": "Multi-Agent",
"createdAt": "2025-06-12T12:35:25.071Z",
"updatedAt": "2025-06-12T12:35:25.071Z"
},
{
"id": "NRBaVI2WatxRn6jb",
"name": "Orchestration",
"createdAt": "2025-06-12T12:35:25.064Z",
"updatedAt": "2025-06-12T12:35:25.064Z"
},
{
"id": "Fz7qIHTqqiYux3E1",
"name": "Human-in-Loop",
"createdAt": "2025-06-12T12:35:25.065Z",
"updatedAt": "2025-06-12T12:35:25.065Z"
}
],
"nodes": [
{
"id": "907e30c8-87e4-46f8-8863-9e3abf429415",
"name": "Start",
"type": "n8n-nodes-base.start",
"position": [
-1600,
300
],
"parameters": {},
"typeVersion": 1
},
{
"id": "ae6910fd-9244-4bab-a0f7-c91a4ad84f28",
"name": "Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"position": [
-1400,
300
],
"parameters": {
"path": "pyragogy/process",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 1
},
{
"id": "a782c52f-b296-4477-8cf4-454a7df0c8a5",
"name": "Check DB Connection",
"type": "n8n-nodes-base.postgres",
"position": [
-1200,
300
],
"parameters": {
"query": "SELECT 1; -- Verifica connessione DB",
"operation": "executeQuery",
"additionalFields": {}
},
"typeVersion": 1
},
{
"id": "3df503dd-db5e-4bd1-8925-e715b8bbe03c",
"name": "Meta-Orchestrator",
"type": "n8n-nodes-base.openAi",
"position": [
-1000,
300
],
"parameters": {
"model": "gpt-4o",
"options": {},
"resource": "chat",
"requestOptions": {}
},
"typeVersion": 1
},
{
"id": "a4c7eb13-bd36-4c9d-8927-2e6fe54c32a3",
"name": "Parse Orchestration Plan",
"type": "n8n-nodes-base.function",
"position": [
-800,
300
],
"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 } }];"
},
"typeVersion": 1
},
{
"id": "915e18ea-1540-4059-883f-739f77f41cc2",
"name": "More Agents to Run?",
"type": "n8n-nodes-base.if",
"position": [
-600,
300
],
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $workflow.currentAgentIndex < $workflow.agentSequence.length }}",
"value2": true
}
]
}
},
"typeVersion": 1
},
{
"id": "6f478d71-791e-4f6b-b5e3-5b7b842181b0",
"name": "Prepare Agent Input",
"type": "n8n-nodes-base.function",
"position": [
-400,
200
],
"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 } }];"
},
"typeVersion": 1
},
{
"id": "d575e9c9-2d58-4061-944b-61b32d15eefb",
"name": "Route Agents with Switch",
"type": "n8n-nodes-base.switch",
"position": [
-200,
200
],
"parameters": {
"mode": "json"
},
"typeVersion": 1
},
{
"id": "3711824c-5a58-4e3d-95dd-3ca16cc551ca",
"name": "Summarizer Agent",
"type": "n8n-nodes-base.openAi",
"position": [
0,
0
],
"parameters": {
"model": "gpt-4o",
"options": {},
"resource": "chat",
"requestOptions": {}
},
"typeVersion": 1
},
{
"id": "67b23742-e3bd-4437-99eb-957e6bc0912e",
"name": "Synthesizer Agent",
"type": "n8n-nodes-base.openAi",
"position": [
0,
100
],
"parameters": {
"model": "gpt-4o",
"options": {},
"resource": "chat",
"requestOptions": {}
},
"typeVersion": 1
},
{
"id": "8bfe179e-ed05-43df-b9c6-862f86c1189c",
"name": "Peer Reviewer Agent",
"type": "n8n-nodes-base.openAi",
"position": [
0,
200
],
"parameters": {
"model": "gpt-4o",
"options": {},
"resource": "chat",
"requestOptions": {}
},
"typeVersion": 1
},
{
"id": "2dc73ca6-f17f-4214-acbb-3ea3ed470f13",
"name": "Sensemaking Agent",
"type": "n8n-nodes-base.openAi",
"position": [
0,
300
],
"parameters": {
"model": "gpt-4o",
"options": {},
"resource": "chat",
"requestOptions": {}
},
"typeVersion": 1
},
{
"id": "ec3b9cc0-a039-4cac-90ca-7de54f41f5d4",
"name": "Prompt Engineer Agent",
"type": "n8n-nodes-base.openAi",
"position": [
0,
400
],
"parameters": {
"model": "gpt-4o",
"options": {},
"resource": "chat",
"requestOptions": {}
},
"typeVersion": 1
},
{
"id": "bc382e04-9c5d-4ab9-b874-2da213499614",
"name": "Onboarding/Explainer Agent",
"type": "n8n-nodes-base.openAi",
"position": [
0,
500
],
"parameters": {
"model": "gpt-4o",
"options": {},
"resource": "chat",
"requestOptions": {}
},
"typeVersion": 1
},
{
"id": "03017738-b11e-4016-8c05-65204ef7e8c3",
"name": "Add Handbook Metadata",
"type": "n8n-nodes-base.function",
"position": [
0,
600
],
"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 } }];"
},
"typeVersion": 1
},
{
"id": "d183d513-00a3-4a38-9ef8-37a646d2ad15",
"name": "Generate Content for Review",
"type": "n8n-nodes-base.function",
"position": [
200,
600
],
"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 } }];"
},
"typeVersion": 1
},
{
"id": "41919337-0762-47d0-9c7c-d51fa966caae",
"name": "Generate Review ID",
"type": "n8n-nodes-base.function",
"position": [
400,
600
],
"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 } }];"
},
"typeVersion": 1
},
{
"id": "6fb5b6d5-4ea8-49c9-9ebc-3c37eb2897b8",
"name": "Send Review Request Email",
"type": "n8n-nodes-base.emailSend",
"position": [
600,
600
],
"parameters": {
"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>",
"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!",
"options": {},
"subject": "Revisione Contenuto Pyragogy Handbook: {{ $json.reviewTitle }}",
"toEmail": "human-reviewer@example.com",
"fromEmail": "your-email@example.com"
},
"typeVersion": 1
},
{
"id": "e69480df-aab3-4b05-8c2b-5ec369966084",
"name": "Wait for Human Approval",
"type": "n8n-nodes-base.wait",
"position": [
800,
600
],
"parameters": {},
"typeVersion": 1
},
{
"id": "80fca66e-fdc6-49b1-9873-b10c03409926",
"name": "Human Decision Split",
"type": "n8n-nodes-base.if",
"position": [
1000,
600
],
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.query.status === 'approved' }}",
"value2": true
}
]
}
},
"typeVersion": 1
},
{
"id": "ed575c60-32e2-411e-9619-ac40ac647e3b",
"name": "Save to handbook_entries",
"type": "n8n-nodes-base.postgres",
"position": [
1200,
500
],
"parameters": {
"table": "handbook_entries",
"columns": "title, content, version, created_by, tags, phase, rhythm",
"additionalFields": {}
},
"typeVersion": 1
},
{
"id": "a43a61ae-0062-4705-8ef2-a65d8bd65fb7",
"name": "Prepare Approved Contribution Data",
"type": "n8n-nodes-base.function",
"position": [
1400,
500
],
"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;"
},
"typeVersion": 1
},
{
"id": "4a03b029-e65f-4844-a605-f668c39f05f7",
"name": "Save Agent Contribution (Approved)",
"type": "n8n-nodes-base.postgres",
"position": [
1600,
500
],
"parameters": {
"table": "agent_contributions",
"columns": "entry_id, agent_name, contribution_type, details",
"additionalFields": {}
},
"typeVersion": 1
},
{
"id": "9a3b2bdc-b894-4b5d-a783-003c1d0cbac5",
"name": "Generate GitHub File Path",
"type": "n8n-nodes-base.function",
"position": [
1800,
500
],
"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 } }];"
},
"typeVersion": 1
},
{
"id": "42831df9-f694-43fa-8612-b9aee95bef6c",
"name": "GitHub Enabled?",
"type": "n8n-nodes-base.if",
"position": [
2000,
500
],
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $env.GITHUB_ACCESS_TOKEN }}",
"value2": true
}
]
}
},
"typeVersion": 1
},
{
"id": "2049ac7d-bf01-4d9b-a222-ca988892d1e6",
"name": "Commit to GitHub (Approved)",
"type": "n8n-nodes-base.github",
"position": [
2200,
500
],
"parameters": {
"owner": "={{ $env.GITHUB_REPOSITORY_OWNER }}",
"filePath": "={{ $json.githubFilePath }}",
"resource": "file",
"operation": "createUpdate",
"repository": "={{ $env.GITHUB_REPOSITORY_NAME }}"
},
"typeVersion": 1
},
{
"id": "352599e9-8b68-412c-bd28-f92844e9c712",
"name": "Log Human Rejection",
"type": "n8n-nodes-base.function",
"position": [
1200,
700
],
"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 } } }];"
},
"typeVersion": 1
},
{
"id": "6f0ea479-d689-40fc-a72f-d1ad161f8743",
"name": "Merge Archivist Paths",
"type": "n8n-nodes-base.merge",
"position": [
2400,
600
],
"parameters": {
"mode": "mergeByPropertyName"
},
"typeVersion": 1
},
{
"id": "ffd0ab23-cb9d-4ac1-ad3d-6fe856ca1fad",
"name": "Evaluate Board Consensus",
"type": "n8n-nodes-base.function",
"position": [
200,
300
],
"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 } }];"
},
"typeVersion": 1
},
{
"id": "f2ec87e5-0a06-4d8f-896b-a3d203fcea41",
"name": "Check Redraft Needed",
"type": "n8n-nodes-base.if",
"position": [
400,
300
],
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.redraftNeeded && $workflow.redraftLoopCount < 2 }}",
"value2": true
}
]
}
},
"typeVersion": 1
},
{
"id": "413074bb-38ad-42d6-8d63-402e33528a5b",
"name": "Handle Redraft",
"type": "n8n-nodes-base.function",
"position": [
600,
200
],
"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 } }];"
},
"typeVersion": 1
},
{
"id": "6508f0f9-09c2-4fe6-8cae-eb90ea58d875",
"name": "Process Agent Output",
"type": "n8n-nodes-base.function",
"position": [
400,
200
],
"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] } }];"
},
"typeVersion": 1
},
{
"id": "2fd3c340-a2d0-489c-a098-42530fdb4c61",
"name": "Slack Enabled?",
"type": "n8n-nodes-base.if",
"position": [
-400,
500
],
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $env.SLACK_WEBHOOK_URL }}",
"value2": true
}
]
}
},
"typeVersion": 1
},
{
"id": "314c7a11-493a-4550-9cd0-f1fc5e5b2994",
"name": "Notify Slack",
"type": "n8n-nodes-base.slack",
"position": [
-200,
500
],
"parameters": {
"text": "Pyragogy AI Village Workflow Completato!\nInput: {{ $json.body.input }}\nOutput Finale: {{ JSON.stringify($json.output) }}\nAgenti Eseguiti: {{ $workflow.agentSequence.join(', ') }}",
"attachments": [],
"otherOptions": {}
},
"typeVersion": 1
},
{
"id": "564a52df-8f4f-4be6-a219-b8c965eccd3b",
"name": "Final Response",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-400,
400
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={{ { finalOutput: $json.output, contributions: $json.contributions, agentSequence: $workflow.agentSequence } }}"
},
"typeVersion": 1
},
{
"id": "generated-ce444b1c-b361-43c8-a0b6-29a1324993c5",
"name": "Workflow Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1600,
-140
],
"parameters": {
"width": 1100,
"height": 400,
"content": "## Pyragogy AI Village Workflow\n\nThis workflow orchestrates an AI Village process for generating, reviewing, and publishing content to a handbook.\n\n**Key Stages:**\n1. **Trigger & DB Check:** Initiates the workflow via webhook and verifies database connection.\n2. **Orchestration:** Meta-Orchestrator plans agent execution.\n3. **Agent Execution Loop:** Dynamically runs various AI agents (Summarizer, Synthesizer, Peer Reviewer, Sensemaking, Prompt Engineer, Onboarding/Explainer) based on the orchestration plan. Includes a redraft mechanism for iterative improvements.\n4. **Human Review:** Content generated by agents (specifically Archivist) undergoes human approval.\n5. **Publishing:** Approved content is saved to a PostgreSQL database and committed to GitHub.\n6. **Notifications:** Sends final response and Slack notifications.\n\n**Note:** Several nodes have credential issues (Postgres, OpenAI, SMTP, GitHub, Slack) and require configuration to function correctly."
}
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "a46219cb-1681-4236-bc4b-9c0c4f58389f",
"connections": {
"Notify Slack": {
"main": [
[
{
"node": "Final Response",
"type": "main",
"index": 0
}
]
]
},
"Handle Redraft": {
"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
}
]
]
},
"GitHub Enabled?": {
"main": [
[
{
"node": "Commit to GitHub (Approved)",
"type": "main",
"index": 0
}
],
[
{
"node": "Merge Archivist Paths",
"type": "main",
"index": 0
}
]
]
},
"Webhook Trigger": {
"main": [
[
{
"node": "Check DB Connection",
"type": "main",
"index": 0
}
]
]
},
"Summarizer Agent": {
"main": [
[
{
"node": "Process Agent Output",
"type": "main",
"index": 0
}
]
]
},
"Meta-Orchestrator": {
"main": [
[
{
"node": "Parse Orchestration Plan",
"type": "main",
"index": 0
}
]
]
},
"Sensemaking Agent": {
"main": [
[
{
"node": "Prompt Engineer Agent",
"type": "main",
"index": 0
}
]
]
},
"Synthesizer Agent": {
"main": [
[
{
"node": "Process Agent Output",
"type": "main",
"index": 0
}
]
]
},
"Generate Review ID": {
"main": [
[
{
"node": "Send Review Request Email",
"type": "main",
"index": 0
}
]
]
},
"Check DB Connection": {
"main": [
[
{
"node": "Meta-Orchestrator",
"type": "main",
"index": 0
}
]
]
},
"Log Human Rejection": {
"main": [
[
{
"node": "Merge Archivist Paths",
"type": "main",
"index": 0
}
]
]
},
"More Agents to Run?": {
"main": [
[
{
"node": "Prepare Agent Input",
"type": "main",
"index": 0
}
],
[
{
"node": "Slack Enabled?",
"type": "main",
"index": 0
}
]
]
},
"Peer Reviewer Agent": {
"main": [
[
{
"node": "Sensemaking Agent",
"type": "main",
"index": 0
}
]
]
},
"Prepare Agent Input": {
"main": [
[
{
"node": "Route Agents with Switch",
"type": "main",
"index": 0
}
]
]
},
"Check Redraft Needed": {
"main": [
[
{
"node": "Handle Redraft",
"type": "main",
"index": 0
}
],
[
{
"node": "Process Agent Output",
"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
}
]
]
},
"Process Agent Output": {
"main": [
[
{
"node": "More Agents to Run?",
"type": "main",
"index": 0
}
]
]
},
"Add Handbook Metadata": {
"main": [
[
{
"node": "Generate Content for Review",
"type": "main",
"index": 0
}
]
]
},
"Merge Archivist Paths": {
"main": [
[
{
"node": "Process Agent Output",
"type": "main",
"index": 0
}
]
]
},
"Prompt Engineer Agent": {
"main": [
[
{
"node": "Evaluate Board Consensus",
"type": "main",
"index": 0
}
]
]
},
"Wait for Human Approval": {
"main": [
[
{
"node": "Human Decision Split",
"type": "main",
"index": 0
}
]
]
},
"Evaluate Board Consensus": {
"main": [
[
{
"node": "Check Redraft Needed",
"type": "main",
"index": 0
}
]
]
},
"Parse Orchestration Plan": {
"main": [
[
{
"node": "More Agents to Run?",
"type": "main",
"index": 0
}
]
]
},
"Route Agents with Switch": {
"main": [
[
{
"node": "Summarizer Agent",
"type": "main",
"index": 0
}
]
]
},
"Save to handbook_entries": {
"main": [
[
{
"node": "Prepare Approved Contribution Data",
"type": "main",
"index": 0
}
]
]
},
"Generate GitHub File Path": {
"main": [
[
{
"node": "GitHub Enabled?",
"type": "main",
"index": 0
}
]
]
},
"Send Review Request Email": {
"main": [
[
{
"node": "Wait for Human Approval",
"type": "main",
"index": 0
}
]
]
},
"Onboarding/Explainer Agent": {
"main": [
[
{
"node": "Process Agent Output",
"type": "main",
"index": 0
}
]
]
},
"Commit to GitHub (Approved)": {
"main": [
[
{
"node": "Merge Archivist Paths",
"type": "main",
"index": 0
}
]
]
},
"Generate Content for Review": {
"main": [
[
{
"node": "Generate Review ID",
"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
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
AI-Driven Handbook Generator with Multi-Agent Orchestration (Pyragogy AI Village). Uses start, postgres, openAi, emailSend. Webhook trigger; 36 nodes.
Source: https://github.com/zengfr/n8n-workflow-all-templates/blob/6a3e60251e39ea8c87e061d82f52633e5d0debe6/n8n-workflow-all-templates/00/00/49/4904_Pyragogy_AI-Driven_Handbook_Generator_with_Multi-Agent_Orchestration.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.
AI-Driven Handbook Generator with Multi-Agent Orchestration (Pyragogy AI Village). Uses start, postgres, openAi, emailSend. Webhook trigger; 36 nodes.
AI-Driven Handbook Generator with Multi-Agent Orchestration (Pyragogy AI Village). 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; 35 nodes.
This n8n workflow orchestrates a powerful suite of AI Agents and automations to manage and optimize various aspects of an e-commerce operation, particularly for platforms like Shopify. It leverages La