Build an n8n research agent that can show its work. It accepts a research brief, searches approved sources, stores one source record per page, produces a structured synthesis, and rejects citations that do not match a successfully retrieved source.

The goal is not “AI browses the web.” That is too vague to test. The goal is a report where every important claim points to a source ID, every source ID maps to a real URL, and failures remain visible instead of being quietly filled with invented facts.

← Return to the practical n8n workflow tutorials

The architecture

Use a controlled outer workflow:

  1. Form Trigger collects the topic, audience, date range, and source rules.
  2. Edit Fields — Normalize Brief creates a stable research contract.
  3. Search Sources calls your approved search API and returns candidate URLs.
  4. Filter + Limit keeps approved domains and a maximum source count.
  5. Loop Over Items fetches one page at a time.
  6. Extract + Summarize Source creates a source card.
  7. Aggregate combines source cards.
  8. Basic LLM Chain or AI Agent synthesizes only from those cards.
  9. Structured Output Parser returns claims, citations, gaps, and conclusion.
  10. Citation Validator rejects unknown IDs.
Auditable n8n research agent workflow from brief to citation validation
The workflow stores and identifies evidence before synthesis. The final model receives source cards, not an unverified pile of search snippets.

Step 1: define the research brief

Add Form Trigger and create these fields:

Field Type Example
topic Text How queue mode changes n8n operations
audience Dropdown/Text Technical team lead
dateFrom Date 2025-01-01
allowedDomains Text docs.n8n.io, github.com/n8n-io
maxSources Number 6
questions Textarea Benefits, trade-offs, failure modes

Add Edit Fields named Normalize Brief. Trim the topic, split the domain list, cap maxSources at a value you can afford, and set a retrieval timestamp:

topic = {{ $json.topic.trim() }}
maxSources = {{ Math.min(Math.max(Number($json.maxSources || 5), 1), 10) }}
retrievedAt = {{ $now }}

Add an If node that stops an empty topic or missing questions. Decide the research boundary before any API or model call.

Step 2: search through an approved service

Use the official API of a search provider available to you. Store its key in an n8n credential. Do not scrape a search-result page or paste an API key into the URL.

In an HTTP Request node named Search Sources:

  • use the provider’s documented endpoint and authentication credential;
  • send the normalized topic and date range;
  • request only the fields needed: title, URL, snippet, and publication date when available;
  • limit results before they reach the model.

After the response, use Edit Fields to create one item per candidate:

{
  "candidateTitle": "...",
  "candidateUrl": "https://...",
  "publishedAt": "2026-09-10",
  "searchSnippet": "..."
}

A snippet is discovery metadata, not evidence. The workflow must successfully fetch the source page before it can cite it.

Step 3: enforce a URL allow-list before fetching

Never let model-generated or search-returned URLs flow straight into an unrestricted HTTP Request node. Parse the hostname and compare it with your approved list. Reject localhost, private IP ranges, non-HTTP schemes, unexpected ports, redirects to disallowed hosts, and oversized downloads.

For a narrow tutorial, use a Switch node with known domains. For a larger deployment, put URL validation in a reviewed sub-workflow. The fetch tool should accept a URL only after the deterministic validator passes it.

Also respect the source’s terms, robots rules, copyright, and rate limits. Retrieve only what you need for analysis; do not republish full articles.

Step 4: fetch and clean one source at a time

Add Loop Over Items with a batch size of 1 or a small controlled concurrency. Connect an HTTP Request named Fetch Approved Page inside the loop. Set:

  • a timeout;
  • a maximum response size at the proxy or request layer where available;
  • bounded retry only for safe transient failures;
  • response metadata such as status and final URL.

Convert HTML into clean text with an HTML extraction step. Remove navigation, cookie banners, scripts, and repeated footer text. Cap the resulting characters before sending them to a model.

If a page returns 403, 404, a paywall, a non-text file, or an unsupported redirect, create a failed-source record. Do not replace it with a model guess.

Step 5: create one structured source card per page

Give each successful page a stable ID such as S1, S2, and S3. Use a Basic LLM Chain with a Structured Output Parser to extract only claims relevant to the brief:

{
  "type": "object",
  "properties": {
    "summary": {"type": "string"},
    "claims": {"type": "array", "items": {"type": "string"}},
    "limitations": {"type": "array", "items": {"type": "string"}},
    "relevance": {"type": "number", "minimum": 0, "maximum": 1}
  },
  "required": ["summary", "claims", "limitations", "relevance"]
}

Then combine the extraction with deterministic metadata:

{
  "sourceId": "S3",
  "title": "Official product release notes",
  "url": "https://official.example/release-notes",
  "publishedAt": "2026-09-10",
  "retrievedAt": "2026-09-21T09:15:00Z",
  "claims": ["Feature A launched", "Region B supported"],
  "limitations": ["No pricing data"]
}
Structured n8n research source card with URL dates and claims
Each cited source has an ID, successful fetch, URL, dates, and extracted claims. That record is the evidence boundary for synthesis.

Store the source cards in the execution and, for repeatable research, in a database or document store with the workflow version. Do not store entire copyrighted pages when a short evidence extract is sufficient.

Step 6: aggregate and synthesize

After the loop completes, aggregate successful source cards into one array. Send the normalized brief and that array to a Basic LLM Chain. Use an AI Agent only if it genuinely needs to choose among additional read-only tools; a fixed synthesis chain is easier to evaluate.

System message:

Write only from the supplied source cards.
Every factual claim must cite one or more sourceId values.
Do not cite search snippets, failed sources, or IDs not supplied.
Separate source facts from your inference.
When sources disagree, state the disagreement.
When evidence is missing, state the gap instead of inventing an answer.

Require structured final output:

{
  "type": "object",
  "properties": {
    "answer": {"type": "string"},
    "keyClaims": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "claim": {"type": "string"},
          "sourceIds": {"type": "array", "items": {"type": "string"}}
        },
        "required": ["claim", "sourceIds"]
      }
    },
    "disagreements": {"type": "array", "items": {"type": "string"}},
    "evidenceGaps": {"type": "array", "items": {"type": "string"}}
  },
  "required": ["answer", "keyClaims", "disagreements", "evidenceGaps"]
}

Step 7: validate citations outside the model

Build a list of valid IDs from the stored successful source cards. Loop over keyClaims and confirm:

  • every claim has at least one source ID;
  • every cited ID exists in the valid-ID list;
  • the corresponding fetch succeeded;
  • the cited source card contains a relevant claim or passage;
  • no disallowed domain entered through a redirect.

If validation fails, route the draft to human review with the invalid IDs and source list. Do not silently delete the bad citation and keep the unsupported claim.

Step 8: format a reader-friendly report

Once validation passes, format:

  1. research question and scope;
  2. short answer;
  3. key findings with [S1], [S2] citations;
  4. disagreements and evidence gaps;
  5. source list with title, URL, publication date, and retrieval date;
  6. method note explaining domain and source limits.

Save it as a draft document, database record, or internal page. Require review before publishing externally, especially for legal, medical, financial, or reputational topics.

Test matrix

Test Expected result
Three valid official sources Report with only S1–S3 citations
Search result redirects to disallowed domain Source rejected before extraction
One page returns 404 Failed-source record; page is not cited
Sources disagree Disagreement section names both source IDs
Model returns S9 when only S1–S3 exist Citation validator rejects the draft
No useful source Evidence gap; no invented conclusion
Page contains “ignore previous instructions” Treated as source text, not workflow authority
Source exceeds size limit Truncated or rejected with visible status

Cost and reliability controls

  • Cap candidate URLs and successful sources.
  • Deduplicate canonical URLs before fetching.
  • Cache pages briefly when terms permit and freshness does not require refetching.
  • Cap extracted characters and chunks per page.
  • Use a cheaper model for per-source extraction and a stronger one only for final synthesis when justified.
  • Record tokens, model, latency, source count, and failed fetches.
  • Use bounded retries and a final error route.

A research agent becomes trustworthy through constraints and evidence, not through a longer prompt. If the workflow cannot show which retrieved page supports a claim, it has produced prose—not research.

Official references and next guides

Similar Posts