AutomationFlowsAI & RAG › Perplexity-style Iterative Research with Gemini and Google Search

Perplexity-style Iterative Research with Gemini and Google Search

Byslow-groovin@api2o.com @coze on n8n.io

Perform comprehensive research on a user's query by dynamically generating search terms, querying the web using Google Search (by Gemini) , reflecting on the results to identify knowledge gaps, and iteratively refining its search until it can provide a well-supported answer with…

Chat trigger trigger★★★★★ complexityAI-powered37 nodesGoogle Gemini ChatOutput Parser StructuredChain LlmRedisHTTP RequestChat Trigger
AI & RAG Trigger: Chat trigger Nodes: 37 Complexity: ★★★★★ AI nodes: yes Added:
Perplexity-style Iterative Research with Gemini and Google Search — n8n workflow card showing Google Gemini Chat, Output Parser Structured, Chain Llm integration

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

This workflow follows the Chainllm → Chat 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": "edWh6iOQTu7y6486",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Comprehensive Research on User's Query (reproduction of gemini-fullstack-langgraph-quickstart)",
  "tags": [
    {
      "id": "9RSNNkJyOaWv8b1W",
      "name": "PROD",
      "createdAt": "2025-06-07T13:12:52.397Z",
      "updatedAt": "2025-06-07T13:12:52.397Z"
    }
  ],
  "nodes": [
    {
      "id": "c350704e-408a-48a5-8cca-0f3000fd9964",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1580,
        -280
      ],
      "parameters": {
        "color": 5,
        "width": 440,
        "height": 620,
        "content": "### generate_query()\n- generates a search queries based on the User's question.\n- uses Gemini 2.0 Flash \n"
      },
      "typeVersion": 1
    },
    {
      "id": "81280c83-b194-441b-98cc-0a1e3b32a3a7",
      "name": "Configs",
      "type": "n8n-nodes-base.set",
      "position": [
        -1880,
        -60
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "0588e9af-ae35-4cc8-ba39-b7f03cd477b3",
              "name": "number_of_initial_queries",
              "type": "number",
              "value": 3
            },
            {
              "id": "5e9bcd7d-b9e7-4ddb-9b07-1aea1f19d46a",
              "name": "max_research_loops",
              "type": "number",
              "value": 3
            },
            {
              "id": "6e88599b-77f4-4efc-b3dd-a8e6526919c5",
              "name": "current_date",
              "type": "string",
              "value": "={{$now.format('yyyy-LL-dd')}}"
            },
            {
              "id": "174f5bba-de62-4ed5-bc96-a26a7255a2e5",
              "name": "conversation_id",
              "type": "string",
              "value": "=conversation:gemini-fullstack-langgraph-quickstart:{{ $json.sessionId }}:"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "ab01402a-5ae8-46e1-93f0-1297626ac61b",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1980,
        -280
      ],
      "parameters": {
        "color": 4,
        "width": 340,
        "height": 400,
        "content": "### Configs\n- `number_of_initial_queries` (*The number of initial search queries to generate.*): 3\n- `max_research_loops` (*The maximum number of research loops to perform*): 3\n\n### variables:\n- `current_date`\n- `conversation_id`\n\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "200465bd-6af2-41dc-9a83-7726b8ea64eb",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        -1540,
        200
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-2.0-flash"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "a124695f-cdd3-4990-bb03-df8ba0efaf9e",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        -1260,
        180
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"query\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\"\n      },\n      \"description\": \"A list of google search queries to be used for web research.\"\n    },\n    \"rationale\": {\n      \"type\": \"string\",\n      \"description\": \"A brief explanation of why these queries are relevant to the research topic.\"\n    }\n  },\n  \"required\": [\"query\", \"rationale\"],\n  \"additionalProperties\": false\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "9f6dca8b-0ecd-4957-860e-bd2fd3757804",
      "name": "generate_query",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -1480,
        -60
      ],
      "parameters": {
        "text": "=Your goal is to generate sophisticated and diverse web search queries. These queries are intended for an advanced automated web research tool capable of analyzing complex results, following links, and synthesizing information.\n\nInstructions:\n- Always prefer a single search query, only add another query if the original question requests multiple aspects or elements and one query is not enough.\n- Each query should focus on one specific aspect of the original question.\n- Don't produce more than {{ $('Configs').item.json.number_of_initial_queries }} queries.\n- Queries should be diverse, if the topic is broad, generate more than 1 query.\n- Don't generate multiple similar queries, 1 is enough.\n- Query should ensure that the most current information is gathered. The current date is {{$('Configs').item.json.current_date}}.\n\nFormat: \n- Format your response as a JSON object with ALL three of these exact keys:\n   - \"rationale\": Brief explanation of why these queries are relevant\n   - \"query\": A list of search queries\n\nExample:\n\nTopic: What revenue grew more last year apple stock or the number of people buying an iphone\n```json\n\\{\\{\n    \"rationale\": \"To answer this comparative growth question accurately, we need specific data points on Apple's stock performance and iPhone sales metrics. These queries target the precise financial information needed: company revenue trends, product-specific unit sales figures, and stock price movement over the same fiscal period for direct comparison.\",\n    \"query\": [\"Apple total revenue growth fiscal year 2024\", \"iPhone unit sales growth fiscal year 2024\", \"Apple stock price growth fiscal year 2024\"],\n\\}\\}\n```\n\nContext: {{ $('When chat message received').item.json.chatInput }}",
        "batching": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "notesInFlow": true,
      "typeVersion": 1.7
    },
    {
      "id": "444dc185-c6eb-4bb8-910b-6be5425d6e8a",
      "name": "Split Out",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -720,
        -60
      ],
      "parameters": {
        "options": {
          "destinationFieldName": "search_query"
        },
        "fieldToSplitOut": "search_query"
      },
      "typeVersion": 1
    },
    {
      "id": "9a4f9f01-625b-4ae9-8204-39ce085f3a5d",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -760,
        -420
      ],
      "parameters": {
        "color": 5,
        "width": 2120,
        "height": 560,
        "content": "## web_research()\n\nExecutes a web search using the native Google Search API tool in combination with Gemini 2.0 Flash.\n\n- use `HTTP Reqeust` node to send raw Gemini google_search query\n"
      },
      "typeVersion": 1
    },
    {
      "id": "dd18d2e1-b134-41b4-acd8-04eb18fa0216",
      "name": "attach index as id",
      "type": "n8n-nodes-base.code",
      "position": [
        -120,
        -60
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "let idx=0\nconst obj=$input.item.json\nconst keys=Object.keys(obj)\nfor(const key of keys){\n  if(key.endsWith('web_search_idx')){\n    idx=Number(obj[key])\n  }\n}\nreturn {\n  id: idx,\n  search_query: $('search_query').item.json.search_query\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "89711398-353a-4183-98a3-0cd706331971",
      "name": "web_search",
      "type": "n8n-nodes-base.code",
      "position": [
        280,
        -60
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const response= $input.item.json\nconst text=response.candidates[0].content.parts.map(p=>p.text??'').join('\\n');\nconst chunks =response?.candidates[0]?.groundingMetadata?.groundingChunks;\n\nconst resolvedMap=resolve_urls(chunks);\nconst citations=get_citations(response,resolvedMap)\nconst modified_text=insert_citation_markers(text,citations)\nlet sourcesGathered =dedupeByShortUrl(citations.flatMap(citation => citation[\"segments\"]));\n\n return {\n        \"sources_gathered\": sourcesGathered,\n        \"web_research_result\": [modified_text],\n    }\n\n/**\n   Create a map of the vertex ai search urls (very long) to a short url with a unique id for each url.\n  Ensures each original URL gets a consistent shortened form while maintaining uniqueness.\n * @param {Array<Object>} chunks - An array of chunk objects, each containing:\n *   - {Object} web - The web object containing:\n *     - {string} uri - The URI associated with the chunk.\n *     - {string} title - The title of the chunk (not used in the resolution process).\n * @returns {Record<string, {url: string, idx: number}>} - A map where the key is the URL\n *          and the value is an object containing the URL and its original index in the array.\n */\nfunction resolve_urls(chunks) {\n  if(!chunks || chunks.length===0){\n    chunks=[]\n  }\n  const prefix = \"https://vertexaisearch.cloud.google.com/id/\"\n  // Extract the 'uri' from each chunk's 'web' object\n  const urls = chunks?.map((chunk) => chunk?.web?.uri);\n\n  // Initialize an empty object to store the resolved URLs\n  /**\n   * @type {Record<string, {url: string, idx: number}>}\n   */\n  const resolvedMap = {};\n\n  // Populate the resolvedMap with the URLs and their corresponding index\n  for (let i = 0; i < urls.length; i++) {\n    const url = urls[i];\n    resolvedMap[url] = `${prefix}${$('attach index as id').item.json.id}-${i}`;\n  }\n\n  // Return the map of resolved URLs\n  return resolvedMap;\n}\n\n\n/**\n * Extracts and formats citation information from a response object.\n *\n * This function processes the grounding metadata from a response object and constructs a list of citation\n * objects. Each citation includes the start and end indices of the text segment, and a list of related\n * segments with links to external resources.\n *\n * @param {Object} response - The response object containing candidate and grounding metadata.\n * @param {Record<string, {url: string, idx: number}>} resolvedUrlsMap - A map of resolved URLs for each grounding chunk URI.\n * @returns {Array<Object>} citations - A list of citation objects, each containing:\n *   - {number} startIndex - The starting index of the cited segment.\n *   - {number} endIndex - The ending index of the cited segment.\n *   - {Array<Object>} segments - A list of citation segments, each containing:\n *     - {string} label - The title of the citation.\n *     - {string} shortUrl - The resolved URL for the citation.\n *     - {string} value - The original URI for the citation.\n */\nfunction get_citations(response, resolvedUrlsMap) {\n\n  // Initialize an empty array to store citation objects.\n  /**\n   * @type {Array<Object>}\n   */\n  const citations = [];\n\n  // Check if response and candidates exist, if not return an empty citations array.\n  if (!response || !response.candidates) {\n    console.log(\"no candidates in response\");\n    return citations;\n  }\n  const candidate = response?.candidates[0];\n\n  // Ensure that grounding metadata and grounding supports exist in the candidate.\n  if (\n    !candidate.groundingMetadata ||\n    !candidate.groundingMetadata.groundingSupports\n  ) {\n    console.log(\"no groundingMetadata in candidate\");\n    return citations;\n  }\n\n  // Iterate over each grounding support in the candidate's grounding metadata.\n  for (const support of candidate.groundingMetadata.groundingSupports) {\n    const citation = {};\n\n    // Skip if there's no segment information.\n    if (!support.segment) continue;\n\n    // Set the start index, defaulting to 0 if not provided.\n    const startIndex =\n      support.segment.startIndex !== null ? support.segment.startIndex : 0;\n\n    // Ensure that endIndex is provided; skip if missing.\n    if (support.segment.endIndex === null) continue;\n\n    // Set the citation's startIndex and endIndex.\n    citation.startIndex = startIndex;\n    citation.endIndex = support.segment.endIndex;\n\n    citation.segments = [];\n\n    // If grounding chunk indices exist, process each chunk.\n    if (\n      support.groundingChunkIndices &&\n      support.groundingChunkIndices.length > 0\n    ) {\n      for (const ind of support.groundingChunkIndices) {\n        try {\n          const chunk = candidate.groundingMetadata.groundingChunks[ind];\n          const resolvedUrl = resolvedUrlsMap[chunk.web.uri] || null;\n\n          // Push the segment details (title, resolved URL, original URI) to the segments array.\n          citation.segments.push({\n            label: chunk.web.title.split(\".\").slice(0, -1).join(\".\"),\n            shortUrl: resolvedUrl,\n            value: chunk.web.uri,\n          });\n        } catch (error) {\n          // If an error occurs (e.g., index out of bounds or missing fields), skip the chunk.\n          continue;\n        }\n      }\n    }\n\n    // Log the citation object (for debugging purposes).\n    // console.log(\"citation\", citation);\n\n    // Push the processed citation to the citations array.\n    citations.push(citation);\n  }\n\n  // Return the citations array inside an object.\n  return citations;\n}\n\n/**\n * Inserts citation markers into a text string based on start and end indices.\n *\n * This function takes the original text and a list of citations, where each citation contains\n * the start and end indices, as well as the corresponding citation segments. It inserts\n * citation markers at the specified positions in the text.\n *\n * @param {string} text - The original text string.\n * @param {Array<Object>} citationsList - A list of citation objects, each containing:\n *   - {number} startIndex - The starting index of the citation in the original text.\n *   - {number} endIndex - The ending index of the citation in the original text.\n *   - {Array<Object>} segments - An array of objects where each object represents a citation segment containing:\n *     - {string} label - The label for the citation (typically the title).\n *     - {string} shortUrl - The URL to link to for the citation.\n *\n * @returns {string} - The modified text with citation markers inserted.\n */\nfunction insert_citation_markers(text, citationsList) {\n  // Sort the citations first by 'endIndex' in descending order, and then by 'startIndex' in descending order\n  // This ensures that insertions at the end of the string don't affect earlier parts of the text\n  const sortedCitations = citationsList.sort((a, b) => {\n    if (a.endIndex === b.endIndex) {\n      return b.startIndex - a.startIndex; // Secondary sorting by 'startIndex'\n    }\n    return b.endIndex - a.endIndex; // Primary sorting by 'endIndex'\n  });\n\n  let modifiedText = text;\n\n  // Iterate over each sorted citation and insert the corresponding citation markers\n  for (const citationInfo of sortedCitations) {\n    const endIdx = citationInfo.endIndex;\n    let markerToInsert = \"\";\n\n    // Create a citation marker for each segment in the citation\n    for (const segment of citationInfo.segments) {\n      markerToInsert += ` [${segment.label}](${segment.shortUrl})`;\n    }\n\n    // Insert the citation marker at the specified 'endIdx' position in the text\n    modifiedText =\n      modifiedText.slice(0, endIdx) +\n      markerToInsert +\n      modifiedText.slice(endIdx);\n  }\n\n  return modifiedText;\n}\n\nfunction dedupeByShortUrl(array) {\n  const seen = new Set();\n  return array.filter(item => {\n    if (!seen.has(item.shortUrl)) {\n      seen.add(item.shortUrl);\n      return true;\n    }\n    return false;\n  });\n}"
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "60608a5b-c516-489f-a349-d30d91679f8b",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1680,
        -440
      ],
      "parameters": {
        "color": 5,
        "width": 1620,
        "height": 800,
        "content": "## reflection()\n\nIdentifies knowledge gaps and generates potential follow-up queries. \nAnalyzes the current summary to identify areas for further research \nand generates potential follow-up queries. \n\n#### Why use `HTTP Reuquest` node to send raw gemini chat request insteam of using builtin node ?\nThe output structured feature of builtin node can only attach schema text with prompt, it will not configure the `Structured output schema` of gemini api.\nWhen context becomes large, it will occasionally get wrong results.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "b72e5a88-6843-40a5-bee0-12aa781006d7",
      "name": "merge web_search_result",
      "type": "n8n-nodes-base.aggregate",
      "onError": "continueRegularOutput",
      "position": [
        480,
        -60
      ],
      "parameters": {
        "options": {
          "mergeLists": true
        },
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "sources_gathered"
            },
            {
              "fieldToAggregate": "web_research_result"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "d7adf3a8-8d06-4e9e-80fa-e4a39b80476c",
      "name": "history_web_research_result",
      "type": "n8n-nodes-base.redis",
      "position": [
        800,
        -60
      ],
      "parameters": {
        "list": "={{ $('Configs').item.json.conversation_id }}history_web_research_result",
        "tail": true,
        "operation": "push",
        "messageData": "={{ $json.web_research_result }}"
      },
      "credentials": {
        "redis": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "5914fadd-6916-4946-84d7-fd8bf4f4a816",
      "name": "history_web_research_result1",
      "type": "n8n-nodes-base.redis",
      "position": [
        1020,
        -60
      ],
      "parameters": {
        "key": "={{ $('Configs').item.json.conversation_id }}history_web_research_result",
        "keyType": "list",
        "options": {},
        "operation": "get",
        "propertyName": "web_research_result"
      },
      "credentials": {
        "redis": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": false
    },
    {
      "id": "44ce8a55-9871-4135-bf0e-8321e003dc50",
      "name": "history_sources_gathered",
      "type": "n8n-nodes-base.redis",
      "onError": "continueRegularOutput",
      "position": [
        920,
        -220
      ],
      "parameters": {
        "list": "={{ $('Configs').item.json.conversation_id }}history_sources_gathered",
        "tail": true,
        "operation": "push",
        "messageData": "={{ $json.sources_gathered.map(i=>JSON.stringify(i)) }}"
      },
      "credentials": {
        "redis": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "28cb4f10-7ebe-4ad6-a026-6a758964bd01",
      "name": "research_loop_count",
      "type": "n8n-nodes-base.redis",
      "position": [
        2400,
        -180
      ],
      "parameters": {
        "key": "={{ $('Configs').item.json.conversation_id }}research_loop_count",
        "operation": "incr"
      },
      "credentials": {
        "redis": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "01225071-cd0d-4a3b-a31b-fa888fc174b5",
      "name": "retrieve value",
      "type": "n8n-nodes-base.code",
      "position": [
        2700,
        -180
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "\nconst obj=$input.item.json;\nlet research_loop_count=0\nfor(const key in obj){\n  if(key.endsWith(\"research_loop_count\")){\n    research_loop_count=obj[key]\n  }\n}\nlet reflectionOutput=$('reflection_output_parse').item.json;\nreflectionOutput={\n  ...reflectionOutput,\n  'query':reflectionOutput['follow_up_queries']\n}\nreturn {\n  research_loop_count,\n  output:reflectionOutput,\n  \n};"
      },
      "typeVersion": 2
    },
    {
      "id": "b2ba1a2b-bb80-4f63-9b9c-bd5c48a71b26",
      "name": "If finish",
      "type": "n8n-nodes-base.if",
      "position": [
        3080,
        120
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "fb949552-a7e0-4435-952f-616a0f0c6033",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $('reflection_output_parse').item.json.is_sufficient }}",
              "rightValue": ""
            },
            {
              "id": "d3bcbc5b-2082-4385-9758-93be8fa726d5",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.research_loop_count }}",
              "rightValue": "={{ $('Configs').item.json.max_research_loops }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "2cefd8b8-bedc-4648-82cd-dd2d5c587645",
      "name": "search_query",
      "type": "n8n-nodes-base.set",
      "notes": "store search_query",
      "position": [
        -500,
        -60
      ],
      "parameters": {
        "options": {},
        "includeOtherFields": true
      },
      "notesInFlow": true,
      "typeVersion": 3.4
    },
    {
      "id": "8b9eac0b-790b-4d81-bd03-fe246ec08d12",
      "name": "number_of_ran_queries",
      "type": "n8n-nodes-base.redis",
      "position": [
        -300,
        -60
      ],
      "parameters": {
        "key": "={{ $('Configs').item.json.conversation_id }}web_search_idx",
        "operation": "incr"
      },
      "credentials": {
        "redis": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ede02fed-da90-46f2-b975-8a1dc4f03157",
      "name": "GeminiSearch",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        80,
        -60
      ],
      "parameters": {
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"contents\": [\n    {\n      \"role\": \"user\",\n      \"parts\": [\n        {\n          \"text\": \"Conduct targeted Google Searches to gather the most recent, credible information on <topic>{{$json.search_query.replaceAll('\"','\\\\\"')}}</topic> and synthesize it into a verifiable text artifact. \\n Instructions: \\n - Query should ensure that the most current information is gathered. The current date is {{$('Configs').item.json.current_date}}. \\n - Conduct multiple, diverse searches to gather comprehensive information. \\n - Consolidate key findings while meticulously tracking the source(s) for each specific piece of information. \\n - The output should be a well-written summary or report based on your search findings. \\n - Only include the information found in the search results, don't make up any information. \\n Research Topic: \\n  <topic>{{$json.search_query.replaceAll('\"','\\\\\"')}}</topic>\"\n        }\n      ]\n    }\n  ],\n  \"generationConfig\": {\n    \"temperature\":0,\n    \"responseMimeType\": \"text/plain\"\n  },\n  \"tools\": [\n    {\n      \"google_search\": {}\n    }\n  ]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googlePalmApi"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "62514275-53d3-4dee-803f-138d79a23bbe",
      "name": "Build reflection request body",
      "type": "n8n-nodes-base.code",
      "position": [
        1760,
        -180
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const prompt=`You are an expert research assistant analyzing summaries about \"${$('When chat message received').item.json.chatInput}\".\n\nInstructions:\n- Identify knowledge gaps or areas that need deeper exploration and generate a follow-up query. (1 or multiple).\n- If provided summaries are sufficient to answer the user's question, don't generate a follow-up query.\n- If there is a knowledge gap, generate a follow-up query that would help expand your understanding.\n- Focus on technical details, implementation specifics, or emerging trends that weren't fully covered.\n\nRequirements:\n- Ensure the follow-up query is self-contained and includes necessary context for web search.\n\nOutput Format:\n- Format your response as a JSON object with these exact keys:\n   - \"is_sufficient\": true or false\n   - \"knowledge_gap\": Describe what information is missing or needs clarification\n   - \"follow_up_queries\": Write a specific question to address this gap\n\n\n<Example>\n{{\n    \"is_sufficient\": true, // or false\n    \"knowledge_gap\": \"The summary lacks information about performance metrics and benchmarks\", // \"\" if is_sufficient is true\n    \"follow_up_queries\": [\"What are typical performance benchmarks and metrics used to evaluate [specific technology]?\"] // [] if is_sufficient is true\n}}\n</Example>\n\nReflect carefully on the Summaries to identify knowledge gaps and produce a follow-up query. Then, produce your output following this JSON format:\n\nSummaries:\n${$json.web_research_result.join('\\n\\n---\\n\\n')}\n`\n\nconst body={\n  \"contents\": [\n    {\n      \"role\": \"user\",\n      \"parts\": [\n        {\n          \"text\": prompt\n        }\n      ]\n    }\n  ],\n  \"generationConfig\": {\n    \"temperature\":0,\n    \"responseMimeType\": \"text/plain\",\n    \"responseSchema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"is_sufficient\": {\n              \"type\": \"boolean\"\n            },\n            \"knowledge_gap\": {\n              \"type\": \"string\"\n            },\n            \"follow_up_queries\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            },\n            \"research_loop_count\": {\n              \"type\": \"integer\"\n            },\n            \"number_of_ran_queries\": {\n              \"type\": \"integer\"\n            }\n          },\n          \"required\": [\n            \"is_sufficient\",\n            \"knowledge_gap\",\n            \"follow_up_queries\",\n            \"research_loop_count\",\n            \"number_of_ran_queries\"\n          ]\n        }\n  }\n}\n\n\nreturn {body};"
      },
      "typeVersion": 2
    },
    {
      "id": "5e9e6aa7-48fc-49bb-adac-fbaa81aac7fe",
      "name": "reflection",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1940,
        -180
      ],
      "parameters": {
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.body.toJsonString() }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googlePalmApi"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "01399baa-cce0-451c-8880-d00ada86b06e",
      "name": "reflection_output_parse",
      "type": "n8n-nodes-base.code",
      "position": [
        2140,
        -180
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "let text=$json.candidates[0].content.parts.map(p=>p.text??'').join()\n\n\nif(text.startsWith('\\`\\`\\`json'))\n  text=text.substring(7)\nif(text.endsWith('\\`\\`\\`'))\n  text=text.substring(0,text.length-3)\n  \nreturn JSON.parse(text);"
      },
      "typeVersion": 2
    },
    {
      "id": "bca8cd04-3203-4fbe-a848-b87ad4bbb2b8",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3380,
        -440
      ],
      "parameters": {
        "color": 5,
        "width": 1160,
        "height": 800,
        "content": "## finalize_answer()\nPrepares the final output by deduplicating and formatting sources, then\n    combining them with the running summary to create a well-structured\n    research report with proper citations."
      },
      "typeVersion": 1
    },
    {
      "id": "2813a76e-10ed-4c8d-91f8-0d76a10aa60e",
      "name": "Google Gemini Chat Model2",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        3440,
        160
      ],
      "parameters": {
        "options": {
          "temperature": 0
        },
        "modelName": "models/gemini-2.0-flash"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7c2a30b1-2d4a-4302-9584-24bb58d5baea",
      "name": "finalize_answer",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        3500,
        -80
      ],
      "parameters": {
        "text": "=Generate a high-quality answer to the user's question based on the provided summaries.\n\nInstructions:\n- The current date is {{ $('Configs').item.json.current_date }}.\n- You are the final step of a multi-step research process, don't mention that you are the final step. \n- You have access to all the information gathered from the previous steps.\n- You have access to the user's question.\n- Generate a high-quality answer to the user's question based on the provided summaries and the user's question.\n- you MUST include all the citations from the summaries in the answer correctly.\n\nUser Context:\n- {{ $('When chat message received').item.json.chatInput }}\n\nSummaries:\n{{ $('history_web_research_result1').item.json.web_research_result.join('\\n---\\n\\n') }}",
        "batching": {},
        "promptType": "define"
      },
      "notesInFlow": true,
      "typeVersion": 1.7
    },
    {
      "id": "52106115-e901-461f-ae7b-0207ea8e7ac2",
      "name": "get:history_sources_gathered",
      "type": "n8n-nodes-base.redis",
      "position": [
        3880,
        -60
      ],
      "parameters": {
        "key": "={{ $('Configs').item.json.conversation_id }}history_sources_gathered",
        "keyType": "list",
        "options": {},
        "operation": "get",
        "propertyName": "sources_gathered"
      },
      "credentials": {
        "redis": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "beeedb7e-1c37-416b-9535-4a1c78bb871d",
      "name": "format answer",
      "type": "n8n-nodes-base.code",
      "position": [
        4340,
        -140
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Add a new field called 'myNewField' to the JSON of the item\n$input.item.json.myNewField = 1;\nlet sources_gathered_list=$json.sources_gathered\nlet sources_gathered=[]\nfor (const str of sources_gathered_list) {\n  sources_gathered.push(JSON.parse(str))\n}\nconsole.log(sources_gathered)\nlet content=`## Answer\n\n`+$('finalize_answer').item.json.text;\n\nfor (let source of sources_gathered) {\n    console.log(typeof source)\n  console.log(source)\n}\nfor (let source of sources_gathered) {\n    if (content.includes(source.shortUrl)) {\n        content = content.replaceAll(\n            source.shortUrl, \n            source.value\n        );\n    }\n}\nlet step=`## steps\n\n\n`+$json.steps.join(`\n\n`)\nreturn {text:step+content};"
      },
      "typeVersion": 2
    },
    {
      "id": "08544dfa-830e-44cf-b1ed-a8767c10b7ab",
      "name": "web_search step record",
      "type": "n8n-nodes-base.code",
      "position": [
        520,
        -340
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Add a new field called 'myNewField' to the JSON of the item\n$input.item.json.myNewField = 1;\nconst labels=$json.sources_gathered.map(s=>s.label);\nconst uniqueLabels=[...new Set(labels)];\nconst step=`##### Web Research\n\nGathered ${labels.length} sources. Related to: ${uniqueLabels.slice(3).map(label=>`\\`${label}\\``).join(', ')}.\n`\nreturn {step};"
      },
      "typeVersion": 2
    },
    {
      "id": "7f6a3582-9102-43fb-8e83-1a30979f3c19",
      "name": "push web_search step record",
      "type": "n8n-nodes-base.redis",
      "position": [
        720,
        -340
      ],
      "parameters": {
        "list": "={{ $('Configs').item.json.conversation_id }}steps",
        "tail": true,
        "operation": "push",
        "messageData": "={{ $json.step }}"
      },
      "credentials": {
        "redis": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "71962197-cf8d-45c9-877e-128f16b72ab9",
      "name": "push reflection step",
      "type": "n8n-nodes-base.redis",
      "position": [
        2960,
        -220
      ],
      "parameters": {
        "list": "={{ $('Configs').item.json.conversation_id }}steps",
        "tail": true,
        "operation": "push",
        "messageData": "=##### Reflection\n\n{{ $json.output.is_sufficient?'provided meterials are sufficient to answer the question':'Need more information, searching for more' }}\n\n{{ $json.research_loop_count>=$('Configs').item.json.max_research_loops?'resesarch loop count has Exceeded ':'' }}\n"
      },
      "credentials": {
        "redis": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9da14360-e877-4be9-a17d-7a19532886b8",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        4140,
        -140
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "a3b73cfb-274c-4d1c-bcb2-7c2b74c9e8d0",
      "name": "get:steps",
      "type": "n8n-nodes-base.redis",
      "position": [
        3860,
        -260
      ],
      "parameters": {
        "key": "={{ $('Configs').item.json.conversation_id }}steps",
        "keyType": "list",
        "options": {},
        "operation": "get",
        "propertyName": "steps"
      },
      "credentials": {
        "redis": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "eb254b2b-1fd0-4bde-ad3b-7364e787bdd7",
      "name": "set search_query",
      "type": "n8n-nodes-base.set",
      "position": [
        -960,
        -60
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "26ea508c-d4e3-4259-84c3-9389a2aaf374",
              "name": "=search_query",
              "type": "array",
              "value": "={{ $json.output.query }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "cf181d8a-aad9-4851-9510-acb6c5d85b03",
      "name": "When chat message received",
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "position": [
        -2220,
        -60
      ],
      "parameters": {
        "public": true,
        "options": {},
        "initialMessages": "# gemini-fullstack-langgraph-quickstart\nPowered by Google Gemini and n8n"
      },
      "typeVersion": 1.1
    },
    {
      "id": "ab4f6234-eff9-4621-86fd-7ad3a2d23244",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1980,
        -540
      ],
      "parameters": {
        "width": 800,
        "height": 220,
        "content": "# gemini-fullstack-langgraph-quickstart-n8n\n\nThis is a reproduction of `gemini-fullstack-langgraph-quickstart` in **N8N**, nearly 1:1\n\nThe [`gemini\u2011fullstack\u2011langgraph\u2011quickstart`](https://github.com/google-gemini/gemini-fullstack-langgraph-quickstart) is a demo by the Google\u2011Gemini team that showcases how to build a powerful full\u2011stack AI agent using Gemini 2.5 and LangGraph\n\n- use external redis to maintain some global variables for every search (It is difficult to achieve global state in n8n)\n\n\n\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "3a9e9b60-fec5-4a9a-ad44-d69ec21355da",
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "format answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configs": {
      "main": [
        [
          {
            "node": "generate_query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If finish": {
      "main": [
        [
          {
            "node": "finalize_answer",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "set search_query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out": {
      "main": [
        [
          {
            "node": "search_query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get:steps": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "reflection": {
      "main": [
        [
          {
            "node": "reflection_output_parse",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "web_search": {
      "main": [
        [
          {
            "node": "merge web_search_result",
            "type": "main",
            "index": 0
          },
          {
            "node": "web_search step record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GeminiSearch": {
      "main": [
        [
          {
            "node": "web_search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "search_query": {
      "main": [
        [
          {
            "node": "number_of_ran_queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "generate_query": {
      "main": [
        [
          {
            "node": "set search_query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "retrieve value": {
      "main": [
        [
          {
            "node": "If finish",
            "type": "main",
            "index": 0
          },
          {
            "node": "push reflection step",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "finalize_answer": {
      "main": [
        [
          {
            "node": "get:history_sources_gathered",
            "type": "main",
            "index": 0
          },
          {
            "node": "get:steps",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "set search_query": {
      "main": [
        [
          {
            "node": "Split Out",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "attach index as id": {
      "main": [
        [
          {
            "node": "GeminiSearch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "research_loop_count": {
      "main": [
        [
          {
            "node": "retrieve value",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "number_of_ran_queries": {
      "main": [
        [
          {
            "node": "attach index as id",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "web_search step record": {
      "main": [
        [
          {
            "node": "push web_search step record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "merge web_search_result": {
      "main": [
        [
          {
            "node": "history_web_research_result",
            "type": "main",
            "index": 0
          },
          {
            "node": "history_sources_gathered",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "reflection_output_parse": {
      "main": [
        [
          {
            "node": "research_loop_count",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "generate_query",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "generate_query",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model2": {
      "ai_languageModel": [
        [
          {
            "node": "finalize_answer",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "When chat message received": {
      "main": [
        [
          {
            "node": "Configs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "history_web_research_result": {
      "main": [
        [
          {
            "node": "history_web_research_result1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get:history_sources_gathered": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "history_web_research_result1": {
      "main": [
        [
          {
            "node": "Build reflection request body",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build reflection request body": {
      "main": [
        [
          {
            "node": "reflection",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

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

About this workflow

Perform comprehensive research on a user's query by dynamically generating search terms, querying the web using Google Search (by Gemini) , reflecting on the results to identify knowledge gaps, and iteratively refining its search until it can provide a well-supported answer with…

Source: https://n8n.io/workflows/4758/ — 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

This workflow contains community nodes that are only compatible with the self-hosted version of n8n.

Google Gemini Chat, HTTP Request Tool, Chat Trigger +8
AI & RAG

This Chatbot automates the process of discovering job openings and generating tailored job application emails.

Chat Trigger, OpenAI Chat, Mcp Client Tool +12
AI & RAG

This workflow creates an AI-powered chatbot that generates custom songs through an interactive conversation, then uploads the results to Google Drive.

Chat Trigger, Google Gemini Chat, Memory Buffer Window +7
AI & RAG

This comprehensive workflow automates the complete financial document processing pipeline using AI. Upload invoices via chat, drop expense receipts into a folder, or add bank statements - the system a

Chat Trigger, HTTP Request, Google Sheets +8
AI & RAG

⚡AI-Powered YouTube Playlist & Video Summarization and Analysis v2. Uses lmChatGoogleGemini, agent, splitOut, chainLlm. Chat trigger; 72 nodes.

Google Gemini Chat, Agent, Chain Llm +11