AutomationFlowsAI & RAG › Paperready — Validate

Paperready — Validate

PaperReady — Validate. Uses httpRequest, agent, lmChatGoogleGemini, vectorStoreQdrant. Webhook trigger; 16 nodes.

Webhook trigger★★★★☆ complexityAI-powered16 nodesHTTP RequestAgentGoogle Gemini ChatQdrant Vector StoreGoogle Gemini EmbeddingsHTTP Request Tool
AI & RAG Trigger: Webhook Nodes: 16 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow follows the Agent → Google Gemini Embeddings 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
{
  "name": "PaperReady \u2014 Validate",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "validate",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "3464dbd4-1929-49ca-bb3f-13be57969644",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        208
      ]
    },
    {
      "parameters": {
        "operation": "pdf",
        "binaryPropertyName": "pdf",
        "options": {}
      },
      "id": "8283e940-086a-4274-b2e8-e32642eeaa61",
      "name": "Extract from File",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        224,
        112
      ]
    },
    {
      "parameters": {
        "jsCode": "// Manuscript parser \u2014 runs inside the n8n \"Code\" node of the validate workflow.\n//\n// Input:  $input.first().json from the upstream \"Extract from File\" node\n//           - .text       full extracted PDF text\n//           - .numpages   page count\n//         $('Webhook').first().json.body.journal_id\n//\n// Output: a single item whose json is { journal_id, manuscript: ParsedManuscript }\n//         The ParsedManuscript shape matches docs/design/n8n-validate-workflow.md.\n//\n// Parsing is intentionally heuristic \u2014 the agent reasons over these fields\n// with retrieved guideline context, so the parser only needs to surface\n// structure, not be authoritative.\n\nconst inputJson = $input.first().json;\nconst text = (inputJson.text || '').toString();\nconst pageCount = inputJson.numpages || 0;\nconst journalId = $('Webhook').first().json.body?.journal_id || null;\n\n// --- Title: first long-ish line in the first ~1500 chars (above the abstract) ---\n// Skip common running headers from preprint servers and conference banners so\n// we don't classify the boilerplate as the title (e.g. ViT's \"Published as a\n// conference paper at ICLR 2021\" appears on line 1, above the real title).\nconst SKIP_HEADER_PATTERNS = [\n  /^abstract\\b/i,\n  /^published\\s+as/i,\n  /^preprint\\b/i,\n  /^under\\s+review/i,\n  /^arxiv:/i,\n  /^manuscript\\s+received/i,\n  /^\\d{4}\\s+ieee/i,\n  /^\u00a9\\s*\\d{4}/i,\n];\nconst firstChunk = text.slice(0, 1500);\nconst firstLines = firstChunk.split('\\n').map(l => l.trim()).filter(Boolean);\nconst title = firstLines.find(l =>\n  l.length > 20 && !SKIP_HEADER_PATTERNS.some(p => p.test(l))\n) || null;\n\n// --- ORCIDs ---\nconst orcidRe = /\\d{4}-\\d{4}-\\d{4}-\\d{3}[\\dX]/g;\nconst orcids = text.match(orcidRe) || [];\n\n// --- Abstract: text between \"Abstract\" and the next major heading ---\nconst absMatch = text.match(/Abstract[\\s\\-:.]+([\\s\\S]{50,2500}?)(?=Keywords|Introduction|1\\.\\s|I\\.\\s|\\n\\n\\n)/i);\nconst abstract = absMatch ? absMatch[1].trim() : null;\n\n// --- Sections: capitalised heading-like lines ---\nconst sectionRe = /^(?:\\d+\\.?\\s+)?([A-Z][A-Za-z ]{3,40})$/gm;\nconst sectionsSet = new Set();\nlet mSec;\nwhile ((mSec = sectionRe.exec(text)) !== null) sectionsSet.add(mSec[1].trim());\nconst sections_detected = Array.from(sectionsSet);\n\n// --- Declarations: simple substring probes ---\nconst lower = text.toLowerCase();\nconst declarations = {\n  conflict_of_interest: /conflict[s]? of interest|competing interest/.test(lower) ? 'found' : 'missing',\n  funding: /\\bfunding\\b|funded by|\\bgrant\\b/.test(lower) ? 'found' : 'missing',\n  data_availability: /data availability|data sharing/.test(lower) ? 'found' : 'missing',\n  ethics: /ethics statement|ethical (review|approval)|irb approval/.test(lower) ? 'found' : 'missing'\n};\n\n// --- References + DOIs ---\n// DOI shape: 10.NNNN/<rest>. We require at least one slash + 3 chars after it\n// so truncated fragments like \"10.1\" don't get classified as DOIs and fed\n// to crossref_verify_doi (which would 404 and crash the agent).\nconst doiRe = /10\\.\\d{4,9}\\/[-._;()\\/:A-Z0-9]{3,}/gi;\nfunction normalizeDoi(d) {\n  // Strip trailing punctuation that often leaks in from reference text\n  return d.replace(/[.,;)\\]>]+$/, '');\n}\nconst refsIdx = text.search(/\\bReferences\\b/i);\nlet refsBlock = refsIdx >= 0 ? text.slice(refsIdx).replace(/^References[\\s\\S]{0,10}/i, '') : '';\nconst rawRefs = refsBlock\n  .split(/\\n\\s*(?:\\[\\d+\\]|\\d+\\.)\\s+/)\n  .map(s => s.trim())\n  .filter(s => s.length > 20)\n  .slice(0, 100);\nconst references = rawRefs.map(raw => {\n  const dm = raw.match(doiRe);\n  return { raw: raw.slice(0, 500), doi: dm ? normalizeDoi(dm[0]) : null };\n});\nconst allDois = (text.match(doiRe) || []).map(normalizeDoi);\n\nreturn [{\n  json: {\n    journal_id: journalId,\n    manuscript: {\n      title,\n      orcids_found: orcids,\n      abstract,\n      sections_detected,\n      declarations,\n      references,\n      stats: {\n        page_count: pageCount,\n        reference_count: references.length,\n        doi_count: allDois.length,\n        orcid_count: orcids.length\n      }\n    }\n  }\n}];\n"
      },
      "id": "efa5dd33-ad69-4b7f-b014-76bb97aa6e93",
      "name": "Parse Manuscript",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        448,
        112
      ]
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineByPosition",
        "options": {}
      },
      "id": "d348709a-c2eb-4c7e-a183-4c12ad60a89a",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        672,
        208
      ]
    },
    {
      "parameters": {
        "url": "=http://host.docker.internal:8000/journals/{{ $json.body.journal_id }}",
        "options": {}
      },
      "id": "f0b912b7-aa52-4128-99af-f10c0e89d342",
      "name": "Get Journal Metadata",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        448,
        304
      ]
    },
    {
      "parameters": {
        "url": "=http://host.docker.internal:8000/requirements/{{ $json.journal_id }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "ce793bb7-54fc-45c9-a094-72a9e742f401",
      "name": "Get Requirements",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        560,
        304
      ]
    },
    {
      "parameters": {
        "jsCode": "// Bundle Context \u2014 runs after the Merge, before the Validator Agent.\n//\n// Input:  $input.first().json = output of \"Merge\" (combineByPosition)\n//           input 0: { journal_id, manuscript } from Parse Manuscript\n//           input 1: requirements JSON from Get Requirements\n//                    ({ journal_id, journal_name, item_count, items[] })\n//         $('Get Journal Metadata').first().json \u2014 full CSV row for the journal\n//\n// Output: a single item bundling { manuscript, journal, requirements } for\n//         the agent. The agent grades each pre-extracted requirement against\n//         the manuscript instead of discovering requirements via RAG.\n\nconst m = $input.first().json || {};\nconst manuscript = m.manuscript || null;\nconst journalRow = $('Get Journal Metadata').first().json || {};\nconst journal = {\n  journal_id: journalRow.journal_id ?? m.journal_id ?? null,\n  name: journalRow.name ?? null,\n  required_reference_style: journalRow.required_reference_style ?? null,\n  issn: journalRow.issn ?? null,\n  reputation_flag: journalRow.reputation_flag ?? null,\n  url: journalRow.homepage_url ?? null\n};\n// The merge will have copied requirements fields onto $json.\nconst requirements = Array.isArray(m.items) ? m.items : [];\n\nreturn [{ json: { manuscript, journal, requirements } }];\n"
      },
      "id": "4764b774-adc1-4757-894d-c60e5132864b",
      "name": "Bundle Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        896,
        208
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Validate the manuscript below against the target journal.\n\nMANUSCRIPT:\n{{ JSON.stringify($json.manuscript, null, 2) }}\n\nTARGET JOURNAL:\n{{ JSON.stringify($json.journal, null, 2) }}\n\nREQUIREMENTS (authoritative \u2014 grade every one as a submission_checklist entry):\n{{ JSON.stringify($json.requirements, null, 2) }}\n\nGrade each requirement against the manuscript. Use your tools only when needed for nuance (DOI verification, reference-style rules, DOAJ lookup, occasional guideline context). Produce the ValidationReport JSON exactly matching the schema in your system message. Output JSON only.",
        "options": {
          "systemMessage": "You are PaperReady, a pre-submission validator for academic manuscripts.\n\nYou receive in the user message:\n  - manuscript: structured fields parsed from the user's PDF (title, authors/ORCIDs,\n    abstract, sections_detected, declarations, references[], stats)\n  - journal: target journal metadata (journal_id, name, required_reference_style,\n    issn, reputation_flag, url)\n  - requirements: a PRE-EXTRACTED list of atomic submission requirements for\n    the target journal. Each item has { topic, requirement, page }. This list\n    was built once from the full author guideline at ingest time \u2014 it is the\n    authoritative checklist. You do NOT need to discover requirements yourself.\n\nYou have FOUR tools. Use them sparingly \u2014 token/rate budget is tight:\n  - qdrant_search(query)        \u2014 semantic search over the target journal's\n                                  official author guideline. Use AT MOST 2 calls\n                                  total, only when a requirement needs guideline\n                                  context to grade.\n  - get_reference_rules(style_name)\n                                \u2014 fetch the journal's expected reference style\n                                  rules. Call ONCE with journal.required_reference_style.\n  - crossref_verify_doi(doi)    \u2014 verify a DOI resolves on Crossref. Call on\n                                  AT MOST 3 sampled DOIs from manuscript.references[].\n  - doaj_lookup(issn)           \u2014 look up the journal in DOAJ by ISSN. Call ONCE\n                                  with journal.issn (skip if issn is null).\n\nYour job has two parts:\n\nPART A \u2014 Grade EVERY item in requirements[].\n  For each input requirement, emit one entry in submission_checklist with:\n    - requirement: copied verbatim from the input\n    - topic: copied verbatim from the input\n    - guideline_page: copied verbatim from the input (the input's \"page\" field)\n    - status: \"pass\" if the manuscript clearly satisfies it,\n              \"fail\" if it clearly violates it,\n              \"warn\" if it partially satisfies or is ambiguous,\n              \"pending\" if it cannot be verified from the manuscript alone\n              (anything about the submission portal, file naming on upload,\n              reviewer suggestions, cover-letter content).\n    - detail: one short sentence explaining the verdict.\n  The output submission_checklist MUST have EXACTLY one entry per requirements\n  item. Do NOT invent extras. Do NOT drop items.\n\nPART B \u2014 Produce five category summaries.\n  Group your gradings into five categories and write a short explanation per\n  category, with at most 3 representative items each (the most important\n  findings, not every checklist row). Categories: reference_style,\n  doi_verification, title_page, declarations, legitimacy. Empty DOAJ results\n  do NOT mean illegitimate.\n\nAlso produce a cover_letter string (150\u2013250 words, plain text with \\n line\nbreaks, no markdown). Address 'Dear Editor-in-Chief,' if no editor name is\nknown. Mention the manuscript title, summarise the contribution from the\nabstract, justify fit using the journal's scope, close with 'Sincerely,\\nThe authors'.\n\nOutput rules:\n  - Output ONLY the ValidationReport JSON. No prose, no markdown fences.\n  - Cite qdrant_search hits by {page, chunk_index_on_page} in evidence_from_guideline.\n  - summary.pass_count, warn_count, fail_count count submission_checklist entries\n    (NOT category items). pending entries don't count toward any of the three.\n  - summary.verdict: \"pass\" if fail_count == 0 and warn_count <= 2,\n                     \"needs_revision\" if fail_count <= 3,\n                     \"fail\" otherwise.\n  - Never fabricate. If a requirement can't be checked from the manuscript, mark\n    it pending.\n\nValidationReport JSON shape:\n{\n  \"journal\": { \"journal_id\": \"...\", \"name\": \"...\", \"required_reference_style\": \"...\" },\n  \"summary\": { \"verdict\": \"pass|needs_revision|fail\",\n               \"pass_count\": 0, \"warn_count\": 0, \"fail_count\": 0 },\n  \"categories\": [\n    {\n      \"id\": \"reference_style|doi_verification|title_page|declarations|legitimacy\",\n      \"title\": \"...\",\n      \"status\": \"pass|warn|fail\",\n      \"explanation\": \"...\",\n      \"evidence_from_guideline\": [\n        { \"page\": 4, \"chunk_index_on_page\": 2, \"excerpt\": \"...\" }\n      ],\n      \"items\": [ { \"label\": \"...\", \"status\": \"...\", \"detail\": \"...\" } ]\n    }\n  ],\n  \"cover_letter\": \"Dear Editor-in-Chief,\\n\\n... (150\u2013250 words) ...\\n\\nSincerely,\\nThe authors\",\n  \"submission_checklist\": [\n    {\n      \"topic\": \"title_page|declarations|references|figures_tables|...\",\n      \"requirement\": \"Copied verbatim from the requirements input\",\n      \"status\": \"pass|warn|fail|pending\",\n      \"detail\": \"One-sentence reason it passed or failed\",\n      \"guideline_page\": 0\n    }\n  ]\n}",
          "maxIterations": 12
        }
      },
      "id": "d548cde4-faed-48b1-8fee-d3a4debee507",
      "name": "Validator Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        1312,
        208
      ]
    },
    {
      "parameters": {
        "modelName": "models/gemini-2.5-flash",
        "options": {
          "temperature": 0.2,
          "maxOutputTokens": 16384
        }
      },
      "id": "e6ae6852-0e38-40d2-8086-9dea399d1bd6",
      "name": "Gemini 2.5 Flash",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "typeVersion": 1.1,
      "position": [
        1120,
        432
      ]
    },
    {
      "parameters": {
        "mode": "retrieve-as-tool",
        "toolDescription": "Semantic search over the target journal's official author guideline (the manuscript's destination journal). Input is a natural-language query like \"title page requirements\" or \"conflict of interest statement\". Returns the top-5 most relevant passages, each with its page number and chunk_index_on_page. Use this for any question about the journal's format requirements.",
        "qdrantCollection": {
          "__rl": true,
          "mode": "list",
          "value": "guideline_chunks",
          "cachedResultName": "guideline_chunks"
        },
        "topK": 5,
        "includeDocumentMetadata": true,
        "options": {
          "searchFilterJson": "={\"must\":[{\"key\":\"metadata.journal_id\",\"match\":{\"value\":\"{{ $('Merge').first().json.journal_id }}\"}}]}",
          "contentPayloadKey": "text"
        }
      },
      "id": "c56e6e6f-7ea1-4a90-bda6-e3b280fbf472",
      "name": "qdrant_search",
      "type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant",
      "typeVersion": 1.3,
      "position": [
        1248,
        432
      ]
    },
    {
      "parameters": {},
      "id": "98239db9-feda-46a3-a322-5c2c5bc7bbb2",
      "name": "Gemini Embeddings",
      "type": "@n8n/n8n-nodes-langchain.embeddingsGoogleGemini",
      "typeVersion": 1,
      "position": [
        1248,
        640
      ]
    },
    {
      "parameters": {
        "toolDescription": "Get all formatting rules for a reference style. Valid style_name values: \"APA 7\", \"IEEE\", \"Vancouver\", \"Harvard\", \"Chicago\". Call once with the journal's required_reference_style.",
        "method": "GET",
        "url": "=http://host.docker.internal:8000/reference-rules/{{ encodeURIComponent($fromAI('style_name', 'Reference style name. One of: APA 7, IEEE, Vancouver, Harvard, Chicago.', 'string')) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "5e9afe60-a12b-405e-a2ac-2646a472d866",
      "name": "get_reference_rules",
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.4,
      "position": [
        1376,
        432
      ]
    },
    {
      "parameters": {
        "toolDescription": "Verify that a DOI resolves to a real Crossref record. Input is a DOI like \"10.1109/CVPR.2022.12345\" (no URL prefix). Returns 200 with metadata if found, 404 if not. Use to check whether references[].doi values are real.",
        "method": "GET",
        "url": "=https://api.crossref.org/works/{{ $fromAI('doi', 'A DOI such as 10.1109/CVPR.2022.12345 (no URL prefix).', 'string') }}",
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "PaperReady/0.1 (mailto:contact@paperready.example)"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "bd6d0752-4a0d-4362-aabf-3ce540df11d4",
      "name": "crossref_verify_doi",
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.4,
      "position": [
        1504,
        432
      ]
    },
    {
      "parameters": {
        "toolDescription": "Look up a journal in DOAJ (Directory of Open Access Journals) by ISSN. Input is an ISSN like \"1939-3539\". Non-empty results signal OA legitimacy. Empty results do NOT mean illegitimate \u2014 many reputable journals are not OA.",
        "method": "GET",
        "url": "=https://doaj.org/api/search/journals/issn:{{ $fromAI('issn', 'An ISSN such as 1939-3539.', 'string') }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "a5951976-834c-435e-8bf0-bc1bd63c0a45",
      "name": "doaj_lookup",
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.4,
      "position": [
        1632,
        432
      ]
    },
    {
      "parameters": {
        "jsCode": "// Clean Output \u2014 runs after the Validator Agent, before Respond to Webhook.\n//\n// The Validator Agent normally emits { output: \"\\`\\`\\`json\\n{...ValidationReport...}\\n\\`\\`\\`\" }\n// \u2014 the report wrapped in a markdown fence inside a string. This node\n// unwraps that and responds with a clean ValidationReport object.\n//\n// Also handles two known Gemini Flash-Lite failure modes:\n//   1) The model \"leaks\" a tool call as plain text instead of issuing a real\n//      function call \u2014 recognisable as \"Calling <tool> with input: {...}\".\n//      We surface a clear, retry-friendly error.\n//   2) The model wraps JSON in extra prose (\"Here is the report: ...\"). We\n//      slice from the first '{' to the last '}' as a fallback.\n\nconst raw = $input.first().json.output;\n\nif (raw && typeof raw === 'object') {\n  return [{ json: raw }];\n}\n\nlet text = String(raw ?? '').trim();\n\nconst toolLeak = text.match(/^Calling\\s+(\\w+)\\s+with\\s+input:/i);\nif (toolLeak) {\n  return [{ json: {\n    error: 'The model leaked a tool call as text instead of completing the report. This is a known Gemini Flash-Lite reliability issue on multi-step agent loops. Please re-submit.',\n    leaked_tool: toolLeak[1],\n    raw,\n  } }];\n}\n\nconst fenceMatch = text.match(/```(?:json)?\\s*\\n?([\\s\\S]*?)\\n?```/i);\nif (fenceMatch) text = fenceMatch[1].trim();\n\nlet parsed;\ntry {\n  parsed = JSON.parse(text);\n} catch (e1) {\n  const first = text.indexOf('{');\n  const last = text.lastIndexOf('}');\n  if (first !== -1 && last > first) {\n    try {\n      parsed = JSON.parse(text.slice(first, last + 1));\n    } catch (e2) {\n      return [{ json: { error: 'failed to parse agent output', message: e2.message, raw } }];\n    }\n  } else {\n    return [{ json: { error: 'failed to parse agent output (no JSON object found)', raw } }];\n  }\n}\n\nreturn [{ json: parsed }];\n"
      },
      "id": "6b6aa007-0472-4d36-b374-615e2d7d7f50",
      "name": "Clean Output",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1840,
        208
      ]
    },
    {
      "parameters": {
        "options": {
          "responseCode": 200
        }
      },
      "id": "7e18ac22-4774-44a4-8fa2-56b4a6ba76b6",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        2064,
        208
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Extract from File",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Journal Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from File": {
      "main": [
        [
          {
            "node": "Parse Manuscript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Manuscript": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Bundle Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Journal Metadata": {
      "main": [
        [
          {
            "node": "Get Requirements",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Requirements": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Bundle Context": {
      "main": [
        [
          {
            "node": "Validator Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validator Agent": {
      "main": [
        [
          {
            "node": "Clean Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Output": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini 2.5 Flash": {
      "ai_languageModel": [
        [
          {
            "node": "Validator Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "qdrant_search": {
      "ai_tool": [
        [
          {
            "node": "Validator Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Gemini Embeddings": {
      "ai_embedding": [
        [
          {
            "node": "qdrant_search",
            "type": "ai_embedding",
            "index": 0
          }
        ]
      ]
    },
    "get_reference_rules": {
      "ai_tool": [
        [
          {
            "node": "Validator Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "crossref_verify_doi": {
      "ai_tool": [
        [
          {
            "node": "Validator Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "doaj_lookup": {
      "ai_tool": [
        [
          {
            "node": "Validator Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": true
  }
}
Pro

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

About this workflow

PaperReady — Validate. Uses httpRequest, agent, lmChatGoogleGemini, vectorStoreQdrant. Webhook trigger; 16 nodes.

Source: https://github.com/rotanakkosal/paper-ready-agentic/blob/9a980e36e12b127771bcfc5b3c734717a5ff64aa/n8n/workflows/validate.json — 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 automates customer chat with Chatwoot & n8n AI agent that handles incoming chats, qualifies leads, answers FAQs from Pinecone knowledge base, and escalates to a live human agent when one

HTTP Request, Memory Postgres Chat, Gmail Tool +7
AI & RAG

Camila IA. Uses postgres, crypto, redis, agent. Webhook trigger; 92 nodes.

Postgres, Crypto, Redis +13
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
AI & RAG

This n8n workflow transforms entire YouTube playlists or single videos into interactive knowledge bases you can chat with. Ask questions and get summaries without needing to watch hours of content. 🔗

Google Gemini Chat, Agent, Chain Llm +11
AI & RAG

HeyDinastia. Uses executeCommand, httpRequest, youTube, postgres. Webhook trigger; 66 nodes.

Execute Command, HTTP Request, YouTube +15