AutomationFlowsAI & RAG › Run a Multi-agent Research and Publishing Pipeline with Gpt-4o, Tavily and…

Run a Multi-agent Research and Publishing Pipeline with Gpt-4o, Tavily and…

Original n8n title: Run a Multi-agent Research and Publishing Pipeline with Gpt-4o, Tavily and Notion

ByMychel Garzon @mychel-garzon on n8n.io

Three specialized AI agents: Researcher, Writer, and Reviewer, collaborate autonomously to research a topic, draft content, and quality-check it through a self-correcting review loop with a circuit breaker. Approved content is published directly to Notion with full audit…

Event trigger★★★★★ complexityAI-powered51 nodesForm TriggerAgentTool Http RequestNotionHTTP RequestError TriggerOpenAI Chat
AI & RAG Trigger: Event Nodes: 51 Complexity: ★★★★★ AI nodes: yes Added:
Run a Multi-agent Research and Publishing Pipeline with Gpt-4o, Tavily and… — n8n workflow card showing Form Trigger, Agent, Tool Http Request integration

This workflow corresponds to n8n.io template #16662 — we link there as the canonical source.

This workflow follows the Agent → Error Trigger 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 →

Download .json
{
  "id": "l2W2kxow4I2CA9L8",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Autonomous Multi-Agent Research & Publishing Pipeline",
  "tags": [],
  "nodes": [
    {
      "id": "c1dc151b-e41f-4265-9093-1eed67cbe574",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4288,
        -2848
      ],
      "parameters": {
        "width": 480,
        "height": 1072,
        "content": "## Autonomous Multi-Agent Research & Publishing Pipeline\n\n### How it works\n\nThis workflow runs an autonomous multi-agent content pipeline from a form submission: it initializes state, routes the requested format, researches the topic, drafts the content, and reviews it until approved or stopped by a circuit breaker. Once ready, it creates a Notion page, appends content blocks in rate-limited batches, records any partial failures as a warning callout, and finishes cleanly. A separate error-trigger path logs workflow failures to a Notion monitoring database.\n\n### Setup steps\n\n- Configure the Form Trigger fields for the topic, requested format, and any other inputs expected by the initialization and routing code.\n- Add OpenAI credentials for all AI chat model nodes used by the researcher, writer, and reviewer agents.\n- Configure Tavily Search access in the researcher agent, including the Tavily API key and any search parameters referenced by the Format Router state.\n- Connect a Notion integration with access to both the content database and monitoring/error database.\n- Set the Notion content and monitoring database IDs referenced by the configuration node or environment variables, such as NOTION_CONTENT_DB_ID and NOTION_MONITORING_DB_ID.\n- Ensure the HTTP Request nodes that append Notion blocks use the correct Notion API authentication, Notion-Version header, and page/block ID expressions.\n\n### Customization\n\nYou can adjust the agent prompts, reviewer approval criteria, circuit breaker limits, Notion block formatting, batch size, and rate-limit wait duration to match different content formats or publishing policies."
      },
      "typeVersion": 1
    },
    {
      "id": "ca649373-b055-48d3-af5b-1c92e6983d59",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4848,
        -2464
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 320,
        "content": "## Capture and route request\n\nStarts from the form submission, initializes the workflow state, and resolves format-specific routing and search configuration before handing off to the agents."
      },
      "typeVersion": 1
    },
    {
      "id": "6e43b825-9761-4705-9969-cab1655eb02e",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5520,
        -2464
      ],
      "parameters": {
        "color": 7,
        "width": 544,
        "height": 528,
        "content": "## Research and validate\n\nThe researcher agent uses OpenAI and Tavily Search to gather source material, then validates the resulting research payload before drafting begins."
      },
      "typeVersion": 1
    },
    {
      "id": "cd15634b-622f-40fc-b5fd-0272491ea8d5",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6096,
        -2464
      ],
      "parameters": {
        "color": 7,
        "width": 352,
        "height": 528,
        "content": "## Draft content writer\n\nThe writer agent and its OpenAI model generate or revise the article draft based on research and reviewer feedback."
      },
      "typeVersion": 1
    },
    {
      "id": "366d00a6-3ba4-42cc-b4bc-9d8bcdb93394",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6480,
        -2464
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 320,
        "content": "## Draft safety gate\n\nValidates each writer pass, increments draft attempt tracking, and routes either toward review or directly to publishing if the circuit breaker is triggered."
      },
      "typeVersion": 1
    },
    {
      "id": "8587d4a3-821c-4806-8d89-d0658e51c660",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        7152,
        -2464
      ],
      "parameters": {
        "color": 7,
        "width": 768,
        "height": 512,
        "content": "## Review approval loop\n\nThe reviewer agent evaluates the draft, validates the review output, and either approves the content for publishing or sends it back to the writer for revision."
      },
      "typeVersion": 1
    },
    {
      "id": "42e7809c-8a85-4b4b-b94b-b39c61add20c",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        7952,
        -2464
      ],
      "parameters": {
        "color": 7,
        "width": 864,
        "height": 512,
        "content": "## Create Notion shell\n\nBuilds the initial Notion page payload, creates the page, carries forward publication state, and merges the created page ID with the content state."
      },
      "typeVersion": 1
    },
    {
      "id": "f952fc4c-6f68-4b5e-8632-6953c8f7bb84",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        8848,
        -2464
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 512,
        "content": "## Prepare append batches\n\nTransforms the merged page and content state into batches of Notion blocks, then starts the batch loop used for appending page content."
      },
      "typeVersion": 1
    },
    {
      "id": "26545508-9963-4c13-a0e3-9a86968d2c11",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        9296,
        -2768
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 432,
        "content": "## Append batch blocks\n\nAppends a batch of content blocks to Notion, handles append errors, and marks each processed batch before continuation checks."
      },
      "typeVersion": 1
    },
    {
      "id": "08eff36c-71c5-4ca9-9584-1369329f7779",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        9968,
        -2768
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 368,
        "content": "## Rate limit looping\n\nDetermines whether the workflow should pause between Notion batch appends, waits when needed, and routes back into the batch loop."
      },
      "typeVersion": 1
    },
    {
      "id": "476be488-ae63-46d1-a28e-fcd17e56b448",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        9296,
        -2304
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 320,
        "content": "## Aggregate publish status\n\nAggregates outcomes from all appended batches, builds a failure warning callout when needed, and decides whether the page requires a warning block."
      },
      "typeVersion": 1
    },
    {
      "id": "3b6dae8a-fe15-4a8b-9d01-a0eff5e64cc0",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        9968,
        -2368
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 384,
        "content": "## Complete with warnings\n\nOptionally appends a warning callout to the Notion page for partial failures, then ends the publishing pipeline."
      },
      "typeVersion": 1
    },
    {
      "id": "40f49753-637b-4a83-9a15-f72a7acef58f",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4848,
        -2112
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 336,
        "content": "## Error monitoring log\n\nSeparate error-triggered cluster that formats workflow failures and logs them to the configured Notion monitoring database."
      },
      "typeVersion": 1
    },
    {
      "id": "a1423bb3-5221-4ea6-83e6-8818c1ad11a5",
      "name": "When Form Submitted",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        4896,
        -2304
      ],
      "parameters": {
        "options": {},
        "formTitle": "Multi-Agent Research Pipeline",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Research Topic",
              "placeholder": "e.g. The impact of AI on B2B SaaS pricing models",
              "requiredField": true
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "Content Format",
              "fieldOptions": {
                "values": [
                  {
                    "option": "blog post"
                  },
                  {
                    "option": "exec summary"
                  },
                  {
                    "option": "LinkedIn post"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldLabel": "Target Audience",
              "placeholder": "e.g. CTOs at mid-market B2B SaaS companies",
              "requiredField": true
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "Max Revision Cycles",
              "fieldOptions": {
                "values": [
                  {
                    "option": "1"
                  },
                  {
                    "option": "2"
                  },
                  {
                    "option": "3"
                  }
                ]
              },
              "requiredField": true
            }
          ]
        },
        "formDescription": "Configure your autonomous research and content generation pipeline"
      },
      "typeVersion": 2.2
    },
    {
      "id": "98cc3513-ad5f-4cad-bead-07d359300c79",
      "name": "Initialize State Variables",
      "type": "n8n-nodes-base.code",
      "position": [
        5120,
        -2304
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// CONFIGURATION \u2014 edit these two values before activating the workflow.\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst NOTION_CONTENT_DB_ID    = \"YOUR_NOTION_CONTENT_DATABASE_ID\";\nconst NOTION_MONITORING_DB_ID = \"YOUR_NOTION_MONITORING_DATABASE_ID\";\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// Form fields arrive on $input directly from Form Trigger.\n// No intermediary Set node \u2014 field names with spaces are preserved exactly.\nconst formData = $input.first().json;\n\n// Validate required fields are present before initialising state.\nconst topic    = formData[\"Research Topic\"];\nconst format   = formData[\"Content Format\"];\nconst audience = formData[\"Target Audience\"];\nconst maxRev   = parseInt(formData[\"Max Revision Cycles\"], 10);\n\nif (!topic)            throw new Error(\"[Initialize State] Missing form field: Research Topic\");\nif (!format)           throw new Error(\"[Initialize State] Missing form field: Content Format\");\nif (!audience)         throw new Error(\"[Initialize State] Missing form field: Target Audience\");\nif (isNaN(maxRev))     throw new Error(\"[Initialize State] Missing or non-numeric: Max Revision Cycles\");\n\nconst state = {\n  topic,\n  format,\n  audience,\n  maxRevisions:            maxRev,\n  revisionCount:           0,\n  draftCount:              0,\n  researchBrief:           null,\n  sources:                 [],\n  keyFacts:                [],\n  contradictions:          [],\n  draft:                   null,\n  draftSections:           [],\n  wordCount:               0,\n  draftTitle:              null,\n  approved:                false,\n  revision_notes:          \"\",\n  accuracyScore:           null,\n  clarityScore:            null,\n  completenessScore:       null,\n  circuitBreakerTriggered: false,\n  circuitBreakerAlert:     false,\n  notionPageId:            null,\n  notionBlocks:            [],\n  batchTotal:              0,\n  // notionMonitoringDbId intentionally omitted \u2014 only used in error workflow\n  // which has no access to pipeline state. Set it directly on Log Error to Notion.\n  notionContentDbId:       NOTION_CONTENT_DB_ID,\n  pipelineStartedAt:       new Date().toISOString()\n};\n\nreturn [{ json: state }];"
      },
      "typeVersion": 2
    },
    {
      "id": "001d6311-5253-4a21-a3b3-d1cfcb6aff29",
      "name": "Resolve Tavily Configuration",
      "type": "n8n-nodes-base.code",
      "position": [
        5344,
        -2304
      ],
      "parameters": {
        "jsCode": "// FIX #1 (Tavily cross-ref): Resolve tavilyConfig here and embed it into state.\n// Tavily Search reads from $json.tavilyConfig \u2014 never from $('Resolve Tavily Configuration').\nconst state = $input.first().json;\n\nconst isShortFormat = state.format === 'LinkedIn post';\n\nreturn [{ json: {\n  ...state,\n  tavilyConfig: {\n    search_depth: isShortFormat ? 'basic' : 'advanced',\n    include_answer: !isShortFormat,\n    max_results: isShortFormat ? 4 : 7\n  }\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "1667c621-d546-4fc5-b8f5-88df810be3a9",
      "name": "Researcher Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        5568,
        -2304
      ],
      "parameters": {
        "text": "=You are a senior research analyst. Conduct thorough research on the following topic for a {{ $json.format }} targeting {{ $json.audience }}.\n\nTOPIC: {{ $json.topic }}\n\nINSTRUCTIONS:\n1. Search for the most relevant, recent, and authoritative sources.\n2. Identify at least 3 high-quality sources.\n3. Extract key facts, statistics, and insights.\n4. Note any contradictions between sources.\n5. Synthesize a research brief appropriate for a {{ $json.format }}.\n\nReturn ONLY valid JSON matching this schema exactly:\n{\n  \"sources\": [{ \"url\": \"string\", \"title\": \"string\", \"excerpt\": \"string\" }],\n  \"keyFacts\": [\"string\"],\n  \"contradictions\": [\"string\"],\n  \"researchBrief\": \"string (150-250 words for LinkedIn post, 300-500 words for other formats)\"\n}\n\nNo preamble, no markdown fences, no explanation.",
        "agent": "conversationalAgent",
        "options": {
          "maxIterations": 5,
          "systemMessage": "You are an expert research analyst. You synthesize information from multiple sources into clear, accurate briefs. You separate facts from speculation. You always return valid JSON."
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "4f3022fd-fa02-460a-a250-d4d702e49da0",
      "name": "Perform Tavily Search",
      "type": "@n8n/n8n-nodes-langchain.toolHttpRequest",
      "position": [
        5712,
        -2080
      ],
      "parameters": {
        "url": "https://api.tavily.com/search",
        "method": "POST",
        "jsonBody": "={\n  \"query\": \"{query}\",\n  \"search_depth\": \"{{ $json.tavilyConfig.search_depth }}\",\n  \"include_answer\": {{ $json.tavilyConfig.include_answer }},\n  \"include_raw_content\": false,\n  \"max_results\": {{ $json.tavilyConfig.max_results }}\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "toolDescription": "Search the web for up-to-date information. The query parameter must be a specific search string.",
        "nodeCredentialType": "tavilyApi"
      },
      "typeVersion": 1.1
    },
    {
      "id": "6a500690-66b6-44d5-9c55-37d8de95e3d9",
      "name": "Verify Research Output",
      "type": "n8n-nodes-base.code",
      "position": [
        5920,
        -2304
      ],
      "parameters": {
        "jsCode": "// Validate Researcher output. State carried entirely via $input.\nconst agentOutput = $input.first().json.output;\nconst baseState = $input.first().json;\n\nlet parsed;\ntry {\n  const cleaned = String(agentOutput)\n    .replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '').replace(/```\\s*$/i, '').trim();\n  parsed = JSON.parse(cleaned);\n} catch (e) {\n  throw new Error(`[Researcher Validation] JSON parse failed: ${e.message} | Raw (first 400): ${String(agentOutput).substring(0, 400)}`);\n}\n\nif (!Array.isArray(parsed.sources))       throw new Error('[Researcher Validation] Missing \"sources\" array');\nif (!Array.isArray(parsed.keyFacts))       throw new Error('[Researcher Validation] Missing \"keyFacts\" array');\nif (!Array.isArray(parsed.contradictions)) throw new Error('[Researcher Validation] Missing \"contradictions\" array');\nif (typeof parsed.researchBrief !== 'string' || parsed.researchBrief.trim().length < 50)\n  throw new Error('[Researcher Validation] Missing or too-short \"researchBrief\"');\nif (parsed.sources.length < 3)\n  throw new Error(`[Researcher Validation] Only ${parsed.sources.length} source(s) found; 3 required`);\n\nreturn [{ json: {\n  ...baseState,\n  output: undefined,\n  sources: parsed.sources,\n  keyFacts: parsed.keyFacts,\n  contradictions: parsed.contradictions,\n  researchBrief: parsed.researchBrief,\n  researchValidatedAt: new Date().toISOString()\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "a012bbdd-e718-4780-8f1b-3be27d2ad6ef",
      "name": "Writer Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        6144,
        -2304
      ],
      "parameters": {
        "text": "=You are an expert content writer. Write a {{ $json.format }} based on the research below.\n\nTARGET AUDIENCE: {{ $json.audience }}\nCONTENT FORMAT: {{ $json.format }}\nTOPIC: {{ $json.topic }}\n\n{% if $json.revision_notes and $json.revision_notes != '' %}\n\u26a0\ufe0f REVISION REQUIRED \u2014 Reviewer rejection #{{ $json.revisionCount }}.\nYou MUST address ALL of these issues specifically:\n\n{{ $json.revision_notes }}\n\nMake substantial, targeted improvements \u2014 do not just reword.\n{% endif %}\n\nRESEARCH BRIEF:\n{{ $json.researchBrief }}\n\nKEY FACTS:\n- {{ $json.keyFacts.join('\\n- ') }}\n\nFORMAT REQUIREMENTS:\n- blog post: 800-1200 words, 4-6 sections with headings, conversational but authoritative\n- exec summary: 400-600 words, 3-4 sections, direct and data-driven\n- LinkedIn post: 150-300 words, 1-2 sections, engaging hook, professional\n\nReturn ONLY valid JSON:\n{\n  \"title\": \"string\",\n  \"sections\": [{ \"heading\": \"string\", \"content\": \"string\" }],\n  \"wordCount\": number,\n  \"draft\": \"string (complete assembled content)\"\n}\n\nNo preamble, no markdown fences.",
        "agent": "conversationalAgent",
        "options": {
          "maxIterations": 3,
          "systemMessage": "You are an expert content writer. You ground all writing in research facts, maintain the correct tone for the format and audience, and directly address all revision notes. You always return valid JSON."
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "a24dbe54-001f-4630-a4f5-2488c9b52007",
      "name": "Verify Draft Content",
      "type": "n8n-nodes-base.code",
      "position": [
        6528,
        -2304
      ],
      "parameters": {
        "jsCode": "// FIX #5 continued: draftCount incremented here (every Writer pass).\n// revisionCount is NOT touched here \u2014 it is incremented only on Reviewer rejection.\n// FIX #9: minimum word count validation uses the stated floor, not 80% of it.\n// A LinkedIn post that passes at 120 words would confuse the Reviewer who expects 150+.\nconst agentOutput = $input.first().json.output;\nconst baseState = $input.first().json;\n\nlet parsed;\ntry {\n  const cleaned = String(agentOutput)\n    .replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '').replace(/```\\s*$/i, '').trim();\n  parsed = JSON.parse(cleaned);\n} catch (e) {\n  throw new Error(`[Writer Validation] JSON parse failed: ${e.message}`);\n}\n\nif (typeof parsed.title !== 'string' || !parsed.title.trim())\n  throw new Error('[Writer Validation] Missing or empty \"title\"');\nif (!Array.isArray(parsed.sections) || parsed.sections.length === 0)\n  throw new Error('[Writer Validation] Missing or empty \"sections\"');\nif (typeof parsed.draft !== 'string' || !parsed.draft.trim())\n  throw new Error('[Writer Validation] Missing or empty \"draft\"');\nif (typeof parsed.wordCount !== 'number')\n  throw new Error('[Writer Validation] Missing or invalid \"wordCount\"');\n\n// FIX #9: use the stated floor exactly, no 80% tolerance.\n// If it fails validation the error surfaces clearly rather than producing\n// an under-spec draft that silently confuses the Reviewer.\nconst minWords = { 'blog post': 800, 'exec summary': 400, 'LinkedIn post': 150 };\nconst actualWords = parsed.draft.split(/\\s+/).filter(w => w.length > 0).length;\nconst formatMin = minWords[baseState.format] || 150;\nif (actualWords < formatMin)\n  throw new Error(`[Writer Validation] Draft too short: ${actualWords} words. Minimum for ${baseState.format} is ${formatMin}.`);\n\nreturn [{ json: {\n  ...baseState,\n  output: undefined,\n  draftTitle: parsed.title,\n  draftSections: parsed.sections,\n  wordCount: actualWords,\n  draft: parsed.draft,\n  draftCount: (baseState.draftCount || 0) + 1\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "31bc06d7-98f1-4d5f-9077-35118750eba3",
      "name": "Break on Writer Review",
      "type": "n8n-nodes-base.code",
      "position": [
        6752,
        -2304
      ],
      "parameters": {
        "jsCode": "// Circuit Breaker \u2014 between Writer and Reviewer.\n// FIX #5: compares revisionCount (Reviewer rejections only) against maxRevisions.\n// On first run revisionCount=0, maxRevisions>=1, so Reviewer always gets at least one shot.\nconst state = $input.first().json;\n\nif (state.revisionCount >= state.maxRevisions) {\n  return [{ json: {\n    ...state,\n    approved: true,\n    circuitBreakerTriggered: true,\n    circuitBreakerAlert: true,\n    approvalReason: `Circuit breaker: ${state.revisionCount} rejection(s) hit the ${state.maxRevisions}-rejection limit. Published as-is.`,\n    accuracyScore: state.accuracyScore ?? 'N/A',\n    clarityScore: state.clarityScore ?? 'N/A',\n    completenessScore: state.completenessScore ?? 'N/A'\n  }}];\n}\n\nreturn [{ json: { ...state, circuitBreakerTriggered: false, circuitBreakerAlert: false } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "bea315ca-632c-47ff-9591-d298907a6ad7",
      "name": "Route for Circuit Breaker Status",
      "type": "n8n-nodes-base.if",
      "position": [
        6976,
        -2304
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "circuit-breaker-check",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.circuitBreakerTriggered }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "b999f3a1-3379-43c5-8010-862103e99047",
      "name": "Reviewer Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        7200,
        -2336
      ],
      "parameters": {
        "text": "=You are a senior editor and fact-checker. Critically review the draft below.\n\nRESEARCH BRIEF (ground truth):\n{{ $json.researchBrief }}\n\nVERIFIED SOURCES:\n{{ $json.sources.map(s => '- ' + s.title + ': ' + s.excerpt).join('\\n') }}\n\nFORMAT: {{ $json.format }} | AUDIENCE: {{ $json.audience }}\n\nDRAFT:\nTitle: {{ $json.draftTitle }}\n\n{{ $json.draft }}\n\nEVALUATION CRITERIA:\n1. ACCURACY (0-10): All facts traceable to sources? Any hallucinations?\n2. CLARITY (0-10): Appropriate for audience and format?\n3. COMPLETENESS (0-10): Covers key facts thoroughly for the format?\n\nAPPROVAL THRESHOLD: ALL three scores must be 7 or above. Be adversarial \u2014 reject on any doubt.\nIf rejecting, provide highly specific, actionable revision notes.\n\nReturn ONLY valid JSON:\n{\n  \"approved\": boolean,\n  \"accuracyScore\": number,\n  \"clarityScore\": number,\n  \"completenessScore\": number,\n  \"revision_notes\": \"string (empty string if approving)\",\n  \"approvalReason\": \"string\"\n}\n\nNo preamble, no markdown fences.",
        "agent": "conversationalAgent",
        "options": {
          "maxIterations": 3,
          "systemMessage": "You are a senior editor and fact-checker, adversarial by design. You catch errors, hallucinations, and quality issues before publication. You cross-reference all claims against the research brief and sources. You approve only when content genuinely meets a high standard. You always return valid JSON."
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "719495c3-bb63-425b-954a-69cf5c7cfee6",
      "name": "Verify Review Output",
      "type": "n8n-nodes-base.code",
      "position": [
        7552,
        -2336
      ],
      "parameters": {
        "jsCode": "// Validate Reviewer output. State from $input only.\n// FIX #5: increment revisionCount here on rejection \u2014 not in Validate Draft Output.\nconst agentOutput = $input.first().json.output;\nconst baseState = $input.first().json;\n\nlet parsed;\ntry {\n  const cleaned = String(agentOutput)\n    .replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '').replace(/```\\s*$/i, '').trim();\n  parsed = JSON.parse(cleaned);\n} catch (e) {\n  throw new Error(`[Reviewer Validation] JSON parse failed: ${e.message}`);\n}\n\nif (typeof parsed.approved !== 'boolean')         throw new Error('[Reviewer Validation] Missing \"approved\" boolean');\nif (typeof parsed.accuracyScore !== 'number')     throw new Error('[Reviewer Validation] Missing \"accuracyScore\"');\nif (typeof parsed.clarityScore !== 'number')      throw new Error('[Reviewer Validation] Missing \"clarityScore\"');\nif (typeof parsed.completenessScore !== 'number') throw new Error('[Reviewer Validation] Missing \"completenessScore\"');\n\n// Only increment revisionCount when the Reviewer rejects.\n// This is the canonical counter the Circuit Breaker checks.\nconst newRevisionCount = parsed.approved\n  ? baseState.revisionCount\n  : (baseState.revisionCount || 0) + 1;\n\nreturn [{ json: {\n  ...baseState,\n  output: undefined,\n  approved: parsed.approved,\n  accuracyScore: parsed.accuracyScore,\n  clarityScore: parsed.clarityScore,\n  completenessScore: parsed.completenessScore,\n  revision_notes: parsed.revision_notes || '',\n  approvalReason: parsed.approvalReason || '',\n  revisionCount: newRevisionCount,\n  reviewValidatedAt: new Date().toISOString()\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "41c75266-c3f1-4d73-87e3-e8cd618e1748",
      "name": "Check Approval Status",
      "type": "n8n-nodes-base.if",
      "position": [
        7776,
        -2336
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "approved-check",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.approved }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "16f41e15-2941-4886-b40d-5ce0e00fffd7",
      "name": "Construct Notion Page",
      "type": "n8n-nodes-base.code",
      "position": [
        8000,
        -2320
      ],
      "parameters": {
        "jsCode": "// Build Notion blocks. State from $input only.\n// FIX #8: batchFailures callout is removed from here \u2014 it cannot be populated\n// until post-loop aggregation runs. The callout is added in Finalize Publish instead.\nconst state = $input.first().json;\n\nconst status = state.circuitBreakerTriggered ? 'Published (Circuit Breaker)' : 'Published';\nconst avgScore = (typeof state.accuracyScore === 'number')\n  ? ((state.accuracyScore + state.clarityScore + state.completenessScore) / 3).toFixed(1)\n  : 'N/A';\n\nconst notionBlocks = [];\n\n// Metadata callout\nconst calloutLines = [\n  `\ud83d\udcca Accuracy: ${state.accuracyScore}/10 | Clarity: ${state.clarityScore}/10 | Completeness: ${state.completenessScore}/10 | Avg: ${avgScore}/10`,\n  `\ud83d\udd04 Reviewer rejections: ${state.revisionCount} / ${state.maxRevisions} max | Writer passes: ${state.draftCount}`,\n  state.circuitBreakerTriggered\n    ? '\u26a0\ufe0f Circuit breaker triggered \u2014 published after max rejections reached'\n    : `\u2705 ${state.approvalReason}`,\n  `\ud83d\udd50 Started: ${state.pipelineStartedAt}`,\n  `\ud83d\udccb Format: ${state.format} | Audience: ${state.audience}`\n];\n\nnotionBlocks.push({\n  object: 'block', type: 'callout',\n  callout: {\n    rich_text: [{ type: 'text', text: { content: calloutLines.join('\\n') } }],\n    icon: { type: 'emoji', emoji: state.circuitBreakerTriggered ? '\u26a0\ufe0f' : '\u2705' },\n    color: state.circuitBreakerTriggered ? 'yellow_background' : 'green_background'\n  }\n});\nnotionBlocks.push({ object: 'block', type: 'divider', divider: {} });\n\n// Content sections\nfor (const section of (state.draftSections || [])) {\n  notionBlocks.push({\n    object: 'block', type: 'heading_2',\n    heading_2: {\n      rich_text: [{ type: 'text', text: { content: section.heading || '' } }],\n      color: 'default'\n    }\n  });\n  const paragraphs = section.content.split(/\\n{2,}/).map(p => p.trim()).filter(Boolean);\n  for (const para of paragraphs) {\n    let remaining = para;\n    while (remaining.length > 1900) {\n      const bp = remaining.lastIndexOf(' ', 1900);\n      const cut = bp > 0 ? bp : 1900;\n      notionBlocks.push({\n        object: 'block', type: 'paragraph',\n        paragraph: { rich_text: [{ type: 'text', text: { content: remaining.substring(0, cut) } }], color: 'default' }\n      });\n      remaining = remaining.substring(cut + 1);\n    }\n    if (remaining.length > 0) {\n      notionBlocks.push({\n        object: 'block', type: 'paragraph',\n        paragraph: { rich_text: [{ type: 'text', text: { content: remaining } }], color: 'default' }\n      });\n    }\n  }\n}\n\n// Sources\nnotionBlocks.push({ object: 'block', type: 'divider', divider: {} });\nnotionBlocks.push({\n  object: 'block', type: 'heading_3',\n  heading_3: { rich_text: [{ type: 'text', text: { content: 'Sources' } }], color: 'default' }\n});\nfor (const src of (state.sources || [])) {\n  notionBlocks.push({\n    object: 'block', type: 'bulleted_list_item',\n    bulleted_list_item: {\n      rich_text: [\n        { type: 'text', text: { content: src.title || src.url }, annotations: { bold: true } },\n        { type: 'text', text: { content: src.url ? ` \u2014 ${src.url}` : '' } }\n      ],\n      color: 'default'\n    }\n  });\n}\n\nreturn [{ json: {\n  ...state,\n  notionPageTitle: state.draftTitle || state.topic,\n  notionStatus: status,\n  notionBlocks,\n  notionAvgScore: avgScore,\n  notionWordCount: state.wordCount,\n  batchTotal: Math.ceil(notionBlocks.length / 25)\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "3acaefd3-beba-45f6-af73-a46afd8d9c0e",
      "name": "Generate Notion Page",
      "type": "n8n-nodes-base.notion",
      "position": [
        8224,
        -2320
      ],
      "parameters": {
        "title": "={{ $json.notionPageTitle }}",
        "pageId": {
          "__rl": true,
          "mode": "url",
          "value": ""
        },
        "options": {}
      },
      "typeVersion": 2.2
    },
    {
      "id": "cf347983-adcc-4253-b5b3-bdcaf9dd8784",
      "name": "Prepare Notion Block Batches",
      "type": "n8n-nodes-base.code",
      "position": [
        8896,
        -2224
      ],
      "parameters": {
        "jsCode": "// Prepare batches for the Notion block append loop.\n// State arrives fully merged via the Merge node input \u2014 no cross-node lookups needed.\n// The merged item contains: .id (Notion page ID from Create Notion Page)\n// plus all state fields (notionBlocks, topic, format, etc.) from the Carry State branch.\nconst input = $input.first().json;\n\nconst pageId = input.id || input.notionPageId;\nconst blocks = input.notionBlocks || [];\n\nconst BATCH_SIZE = 25;\nconst batches = [];\nfor (let i = 0; i < blocks.length; i += BATCH_SIZE) {\n  batches.push({\n    pageId,\n    blocks: blocks.slice(i, i + BATCH_SIZE),\n    batchIndex: Math.floor(i / BATCH_SIZE),\n    totalBatches: Math.ceil(blocks.length / BATCH_SIZE),\n    batchFailed: false,\n    _topic: input.topic,\n    _format: input.format,\n    _circuitBreakerTriggered: input.circuitBreakerTriggered\n  });\n}\n\nreturn batches.map(b => ({ json: b }));"
      },
      "typeVersion": 2
    },
    {
      "id": "e8ba6cac-a429-4a0b-8800-da9d86dc3885",
      "name": "Set State Fields",
      "type": "n8n-nodes-base.set",
      "position": [
        8224,
        -2128
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "carry-state-1",
              "name": "notionBlocks",
              "type": "array",
              "value": "={{ $json.notionBlocks }}"
            },
            {
              "id": "carry-state-2",
              "name": "topic",
              "type": "string",
              "value": "={{ $json.topic }}"
            },
            {
              "id": "carry-state-3",
              "name": "format",
              "type": "string",
              "value": "={{ $json.format }}"
            },
            {
              "id": "carry-state-4",
              "name": "circuitBreakerTriggered",
              "type": "boolean",
              "value": "={{ $json.circuitBreakerTriggered }}"
            },
            {
              "id": "carry-state-5",
              "name": "notionPageTitle",
              "type": "string",
              "value": "={{ $json.notionPageTitle }}"
            },
            {
              "id": "carry-state-6",
              "name": "batchTotal",
              "type": "number",
              "value": "={{ $json.batchTotal }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "82d39f0e-8770-49ba-b56d-49682fe650bf",
      "name": "Combine Page ID with State",
      "type": "n8n-nodes-base.merge",
      "onError": "continueErrorOutput",
      "position": [
        8448,
        -2224
      ],
      "parameters": {
        "mode": "combine",
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "3fa58cf3-a0d6-4439-9c9f-60a0a5dd4091",
      "name": "Process in Batch Loop",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        9120,
        -2224
      ],
      "parameters": {
        "options": {
          "reset": false
        }
      },
      "typeVersion": 3
    },
    {
      "id": "f90a20dc-5895-40ac-a631-bf071f91aeb8",
      "name": "Append Blocks to Notion",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "position": [
        9344,
        -2608
      ],
      "parameters": {
        "url": "=https://api.notion.com/v1/blocks/{{ $json.pageId }}/children",
        "method": "PATCH",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ children: $json.blocks }) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Notion-Version",
              "value": "2022-06-28"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "notionApi"
      },
      "typeVersion": 4.2
    },
    {
      "id": "42bcc413-67ce-4e27-b25c-a974f36bafd5",
      "name": "Handle Batch Errors",
      "type": "n8n-nodes-base.code",
      "position": [
        9568,
        -2512
      ],
      "parameters": {
        "jsCode": "// FIX #3 / #8: Batch failure handler.\n// Records the failure on the item; this feeds back into the loop.\n// Failures are aggregated in Finalize Publish after the loop completes.\nconst errorItem = $input.first().json;\n\nreturn [{ json: {\n  ...errorItem,\n  batchFailed: true,\n  failureRecord: {\n    batchIndex: errorItem.batchIndex,\n    totalBatches: errorItem.totalBatches,\n    pageId: errorItem.pageId,\n    errorMessage: errorItem.error?.message || 'Unknown Notion API error',\n    errorStatus: errorItem.error?.httpCode || 'unknown',\n    failedAt: new Date().toISOString()\n  }\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "023a295e-69fe-4073-a9da-8b11e0361ad4",
      "name": "Verify Additional Batches",
      "type": "n8n-nodes-base.code",
      "position": [
        9792,
        -2608
      ],
      "parameters": {
        "jsCode": "// Tag every item \u2014 success or error path \u2014 with batchProcessed=true.\n// Finalize Publish uses this to confirm an item went through the loop.\n// batchFailed is set by Batch Error Handler; undefined defaults to false here.\nconst item = $input.first().json;\n\nreturn [{ json: {\n  ...item,\n  batchFailed:     item.batchFailed === true,\n  batchProcessed:  true,\n  _shouldWait:     (item.batchIndex ?? 0) < ((item.totalBatches ?? 1) - 1)\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ea209172-1db8-42dd-a16c-9512d0cc52f2",
      "name": "Check Wait Requirement",
      "type": "n8n-nodes-base.if",
      "position": [
        10016,
        -2608
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "should-wait-check",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json._shouldWait }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "303f33bc-d73a-421f-ab60-97c2248b8868",
      "name": "Wait 350 Seconds",
      "type": "n8n-nodes-base.wait",
      "position": [
        10240,
        -2560
      ],
      "parameters": {
        "amount": 350
      },
      "typeVersion": 1.1
    },
    {
      "id": "30750295-33c2-4279-a26e-f7ce4b777882",
      "name": "Finalize Publishing Process",
      "type": "n8n-nodes-base.code",
      "position": [
        9344,
        -2144
      ],
      "parameters": {
        "jsCode": "// Aggregate all batch outcomes from the Loop done output.\n// splitInBatches done-output carries all items that passed through the loop,\n// whether they went through the success path or the error path.\n// batchProcessed=true (set by Check More Batches) is the reliable membership check.\nconst allItems = $input.all();\n\nconst processed = allItems.filter(item => item.json.batchProcessed === true);\nconst failures  = processed\n  .filter(item => item.json.batchFailed === true)\n  .map(item => item.json.failureRecord || {\n    batchIndex:   item.json.batchIndex,\n    errorMessage: \"No failure record \u2014 item rerouted before Batch Error Handler attached one\",\n    failedAt:     new Date().toISOString()\n  });\n\nconst totalBatches = processed[0]?.json.totalBatches ?? processed.length;\nconst pageId  = processed[0]?.json.pageId  || \"\";\nconst topic   = processed[0]?.json._topic  || \"\";\nconst format  = processed[0]?.json._format || \"\";\n\nreturn [{ json: {\n  pageId,\n  topic,\n  format,\n  totalBatches,\n  processedCount:    processed.length,\n  successfulBatches: totalBatches - failures.length,\n  batchFailures:     failures,\n  publishStatus:     failures.length === 0\n    ? \"fully_published\"\n    : failures.length === totalBatches\n      ? \"publish_failed\"\n      : \"partially_published\",\n  finalizedAt: new Date().toISOString()\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9de4f823-cf6e-4422-9832-55ec57b69a57",
      "name": "Construct Failure Alert",
      "type": "n8n-nodes-base.code",
      "position": [
        9568,
        -2144
      ],
      "parameters": {
        "jsCode": "// NOTE: Append Warning Callout (downstream) routes both success and error outputs\n// to Pipeline Complete. A failure to write the warning callout is intentionally\n// swallowed \u2014 it is a non-critical diagnostic write. If you need visibility on\n// these failures, add a Slack or email node on the error output of Append Warning Callout.\n\n// FIX #8 continued: if there were batch failures, append a warning callout\n// to the Notion page now that we have the aggregated failure list.\n// If no failures, this node passes through cleanly.\nconst result = $input.first().json;\n\nif (!result.batchFailures || result.batchFailures.length === 0) {\n  return [{ json: { ...result, warningAppended: false } }];\n}\n\n// Build a warning callout block to append\nconst failureSummary = result.batchFailures\n  .map(f => `Batch ${f.batchIndex + 1}/${f.totalBatches}: ${f.errorMessage} (${f.errorStatus}) at ${f.failedAt}`)\n  .join('\\n');\n\nreturn [{ json: {\n  ...result,\n  warningBlock: {\n    object: 'block',\n    type: 'callout',\n    callout: {\n      rich_text: [{ type: 'text', text: { content:\n        `\u26a0\ufe0f Partial publish: ${result.batchFailures.length} of ${result.totalBatches} batch(es) failed.\\n${failureSummary}`\n      }}],\n      icon: { type: 'emoji', emoji: '\u26a0\ufe0f' },\n      color: 'orange_background'\n    }\n  },\n  warningAppended: true\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "4a44ef00-90c0-4495-8a1b-9b9882c9d68a",
      "name": "Check for Failures",
      "type": "n8n-nodes-base.if",
      "position": [
        9792,
        -2144
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "has-failures",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.warningAppended }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "51524bf4-d67c-4f81-9ae7-f5b41a9c0b65",
      "name": "Add Warning Notification",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "position": [
        10016,
        -2144
      ],
      "parameters": {
        "url": "=https://api.notion.com/v1/blocks/{{ $json.pageId }}/children",
        "method": "PATCH",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ children: [$json.warningBlock] }) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Notion-Version",
              "value": "2022-06-28"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "notionApi"
      },
      "typeVersion": 4.2
    },
    {
      "id": "4a48ead9-aec4-4494-8aa5-639c7c660231",
      "name": "When Error Occurs",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        4896,
        -1952
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "84dcbc24-72a8-4690-abc1-7526426dc542",
      "name": "Create Error Log",
      "type": "n8n-nodes-base.code",
      "position": [
        5120,
        -1952
      ],
      "parameters": {
        "jsCode": "// ERROR WORKFLOW SAFETY NOTE:\n// errorWorkflow is set to \"SELF\". n8n prevents recursive error loops:\n// if this error path itself fails, n8n will NOT re-trigger the error workflow.\n// The failure will appear in n8n execution logs but will not cause infinite recursion.\n// Assumption: Log Error to Notion credential is valid. If not, the error is logged\n// in n8n execution history only. Check Settings > Executions if Notion logs are missing.\n//\n// MONITORING DB ID: set YOUR_NOTION_MONITORING_DATABASE_ID directly on the\n// \"Log Error to Notion\" node. The error trigger has no access to pipeline state.\n\n// FIX #6: Use the correct n8n error trigger payload structure.\n// The error trigger provides: $json.execution (id, url, mode, retryOf),\n// $json.workflow (id, name), and $json.error (message, stack, name, node.name).\n// We do NOT try to read runtime state from the error payload \u2014 it's not there.\n// Instead we log what IS reliably available and make the Notion entry actionable.\nconst e = $input.first().json;\n\nconst failedNode  = e.error?.node?.name   || 'unknown node';\nconst errorMsg    = e.error?.message       || 'No message';\nconst errorName   = e.error?.name          || 'Error';\nconst errorStack  = (e.error?.stack || '').substring(0, 800);\nconst executionId = e.execution?.id        || 'unknown';\nconst executionUrl= e.execution?.url       || '';\nconst workflowName= e.workflow?.name       || 'Multi-Agent Research Pipeline';\n\nconst logTitle = `[PIPELINE ERROR] ${errorName} in \"${failedNode}\" \u2014 ${new Date().toISOString().substring(0,16)}`;\n\nreturn [{ json: {\n  logTitle,\n  workflowName,\n  executionId,\n  executionUrl,\n  failedNode,\n  errorName,\n  errorMessage: errorMsg,\n  errorStack,\n  // Actionable instructions for the engineer reviewing the log\n  actionRequired: `Open execution ${executionId} in n8n (${executionUrl}) to inspect the full item data at node \"${failedNode}\".`,\n  loggedAt: new Date().toISOString()\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "58962798-626c-455f-8b89-4ba4dc2c73cf",
      "name": "Log to Notion on Error",
      "type": "n8n-nodes-base.notion",
      "position": [
        5344,
        -1952
      ],
      "parameters": {
        "title": "={{ $json.logTitle }}",
        "pageId": {
          "__rl": true,
          "mode": "url",
          "value": ""
        },
        "blockUi": {
          "blockValues": [
            {
              "textContent": "={{ $json.actionRequired }}"
            },
            {
              "textContent": "={{ $json.errorStack }}"
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 2.2
    },
    {
      "id": "7d633e8e-b73a-4212-b6f2-186a6338699c",
      "name": "Extract Combined State",
      "type": "n8n-nodes-base.code",
      "position": [
        8672,
        -2224
      ],
      "parameters": {
        "jsCode": "// Merge Page ID + State combined two items by position:\n//   input0 (Create Notion Page): contributes .id plus Notion API metadata\n//   input1 (Carry State):        contributes notionBlocks, topic, format, etc.\n// mergeByPosition merges all fields into one item.\nconst merged = $input.first().json;\n\nconst pageId = merged.id;\nif (!pageId) {\n  throw new Error(\n    \"[Extract Merged State] Notion page ID (.id) is missing from the merged item. \" +\n    \"Likely cause: Create Notion Page failed or returned an unexpected response. \" +\n    \"Check: (1) Notion OAuth2 credentials are valid, \" +\n    \"(2) NOTION_CONTENT_DB_ID in Initialize State is a real database ID, \" +\n    \"(3) the Notion integration has access to that database.\"\n  );\n}\n\nreturn [{ json: { ...merged, notionPageId: pageId } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "3897e3cb-2f7d-485c-b49c-2e6b5db1b7ef",
      "name": "Complete Pipeline Process",
      "type": "n8n-nodes-base.noOp",
      "position": [
        10240,
        -2208
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "5e90d263-55c9-4e38-9b1c-7b46641ea16f",
      "name": "OpenAI GPT-4 Agent 1",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        5584,
        -2080
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "gpt-4o"
        },
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "5f608feb-50a8-4d8c-b30c-108f51a66619",
      "name": "OpenAI GPT-4 Agent 2",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        6224,
        -2080
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "gpt-4o"
        },
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "248260ea-4154-43c8-aa5f-f360526c160b",
      "name": "OpenAI GPT-4 Agent 3",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        7312,
        -2112
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "gpt-4o"
        },
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "747d2d3f-509f-4fb5-9b4f-53de7723d516",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4848,
        -2864
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 368,
        "content": "## Configuration reference\n\nNon-executed reference node documenting required Notion database IDs and configuration values used elsewhere in the pipeline."
      },
      "typeVersion": 1
    },
    {
      "id": "dda31e7a-47ad-4aba-8aad-d549ff9c8a4f",
      "name": "Set Configuration References",
      "type": "n8n-nodes-base.set",
      "position": [
        4992,
        -2656
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg1",
              "name": "NOTION_CONTENT_DB_ID",
              "type": "string",
              "value": "Replace NOTION_CONTENT_DB_ID in Initialize State code node"
            },
            {
              "id": "cfg2",
              "name": "NOTION_MONITORING_DB_ID",
              "type": "string",
              "value": "Replace NOTION_MONITORING_DB_ID in Log Error to Notion node"
            }
          ]
        }
      },
      "typeVersion": 3.4
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "209cf39f-bcd8-4dbf-98a8-4cb3ca4b1624",
  "nodeGroups": [],
  "connections": {
    "Writer Agent": {
      "main": [
        [
          {
            "node": "Verify Draft Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reviewer Agent": {
      "main": [
        [
          {
            "node": "Verify Review Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Error Log": {
      "main": [
        [
          {
            "node": "Log to Notion on Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Researcher Agent": {
      "main": [
        [
          {
            "node": "Verify Research Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set State Fields": {
      "main": [
        [
          {
            "node": "Combine Page ID with State",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Wait 350 Seconds": {
      "main": [
        [
          {
            "node": "Process in Batch Loop",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Error Occurs": {
      "main": [
        [
          {
            "node": "Create Error Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check for Failures": {
      "main": [
        [
          {
            "node": "Add Warning Notification",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Complete Pipeline Process",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Handle Batch Errors": {
      "main": [
        [
          {
            "node": "Verify Additional Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Form Submitted": {
      "main": [
        [
          {
            "node": "Initialize State Variables",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Notion Page": {
      "main": [
        [
          {
            "node": "Combine Page ID with State",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI GPT-4 Agent 1": {
      "ai_languageModel": [
        [
          {
            "node": "Researcher Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI GPT-4 Agent 2": {
      "ai_languageModel": [
        [
          {
            "node": "Writer Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI GPT-4 Agent 3": {
      "ai_languageModel": [
        [
          {
            "node": "Reviewer Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Verify Draft Content": {
      "main": [
        [
          {
            "node": "Break on Writer Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Review Output": {
      "main": [
        [
          {
            "node": "Check Approval Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Approval Status": {
      "main": [
        [
          {
            "node": "Construct Notion Page",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Writer Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Construct Notion Page": {
      "main": [
        [
          {
            "node": "Generate Notion Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Perform Tavily Search": {
      "ai_tool": [
        [
          {
            "node": "Researcher Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Process in Batch Loop": {
      "main": [
        [
          {
            "node": "Append Blocks to Notion",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Finalize Publishing Process",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Break on Writer Review": {
      "main": [
        [
          {
            "node": "Route for Circuit Breaker Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Wait Requirement": {
      "main": [
        [
          {
            "node": "Wait 350 Seconds",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Process in Batch Loop",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Combined State": {
      "main": [
        [
          {
            "node": "Prepare Notion Block Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Research Output": {
      "main": [
        [
          {
            "node": "Writer Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Blocks to Notion": {
      "main": [
        [
          {
            "node": "Verify Additional Batches",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Batch Errors",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Construct Failure Alert": {
      "main": [
        [
          {
            "node": "Check for Failures",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Warning Notification": {
      "main": [
        [
          {
            "node": "Complete Pipeline Process",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Additional Batches": {
      "main": [
        [
          {
            "node": "Check Wait Requirement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Page ID with State": {
      "main": [
        [
          {
            "node": "Extract Combined State",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Initialize State Variables": {
      "main": [
        [
          {
            "node": "Resolve Tavily Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Finalize Publishing Process": {
      "main": [
        [
          {
            "node": "Construct Failure Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Notion Block Batches": {
      "main": [
        [
          {
            "node": "Process in Batch Loop",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve
Pro

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

About this workflow

Three specialized AI agents: Researcher, Writer, and Reviewer, collaborate autonomously to research a topic, draft content, and quality-check it through a self-correcting review loop with a circuit breaker. Approved content is published directly to Notion with full audit…

Source: https://n8n.io/workflows/16662/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

n8n-2-1: Content Team — Notion. Uses formTrigger, chainLlm, lmChatOpenAi, outputParserStructured. Event-driven trigger; 18 nodes.

Form Trigger, Chain Llm, OpenAI Chat +6
AI & RAG

Lection 9 main. Uses formTrigger, chatTrigger, agent, lmChatOpenAi. Event-driven trigger; 55 nodes.

Form Trigger, Chat Trigger, Agent +7
AI & RAG

This n8n workflow demonstrates a simple multi-agent setup to perform the task of competitor research. It showcases how using the HTTP request tool could reduce the number of nodes needed to achieve a

OpenAI Chat, Tool Http Request, Output Parser Structured +3
AI & RAG

Transform your manual hiring process into an intelligent evaluation system that saves 15-20 minutes per candidate! This workflow automates the entire candidate assessment pipeline - from CSV/XLSX uplo

Form Trigger, Google Sheets, Google Drive +8
AI & RAG

Paste any webinar, training, or lecture recording URL into a simple form along with the title, speaker name, topic category, and date and the workflow builds your knowledge base automatically. WayinVi

Form Trigger, HTTP Request, Agent +3