AutomationFlowsAI & RAG › Generate Personalized Patient Education Reports with Google Sheets, Gemini,…

Generate Personalized Patient Education Reports with Google Sheets, Gemini,…

Original n8n title: Generate Personalized Patient Education Reports with Google Sheets, Gemini, Brave, and Google Docs

ByHiroyuki Yamasaki @hxshanqi on n8n.io

Generate personalized, print-ready patient education documents from physician-provided diagnoses, treatment plans, and messages. The workflow finds relevant trusted medical information with Brave Search, uses Gemini to create patient-friendly explanations without overriding the…

Event trigger★★★★☆ complexityAI-powered24 nodesGoogle SheetsGoogle Gemini ChatHTTP RequestChain LlmGoogle Docs
AI & RAG Trigger: Event Nodes: 24 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Chainllm → Google Sheets 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": "BsFSlMxDd1v4lgsn",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Personalized Patient Education Generator (Verified Creator Template)",
  "tags": [],
  "nodes": [
    {
      "id": "95195d47-c319-498f-a1ca-cb8054eda9f4",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -800,
        -352
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "be8ee2eb-6cf7-41b7-ba1a-70f240b03521",
      "name": "Get Pending Patient Education Request",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -576,
        -352
      ],
      "parameters": {
        "options": {
          "returnFirstMatch": true
        },
        "filtersUI": {
          "values": [
            {
              "lookupValue": "pending",
              "lookupColumn": "Status"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1mZ-2TlSSvYFXlKPsCkQlIy7K5ekbRPYyftB2l-9D3v0/edit#gid=0",
          "cachedResultName": "Patient Education Requests"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1mZ-2TlSSvYFXlKPsCkQlIy7K5ekbRPYyftB2l-9D3v0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1mZ-2TlSSvYFXlKPsCkQlIy7K5ekbRPYyftB2l-9D3v0/edit?usp=drivesdk",
          "cachedResultName": "AI Personalized Patient Education Generator"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "6bacff03-13e7-40dd-b165-fde568557a77",
      "name": "Validate Patient Education Input",
      "type": "n8n-nodes-base.code",
      "position": [
        -384,
        -352
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nconst requiredFields = [\n  'Diagnosis',\n  'Physician Plan',\n  'Physician Message',\n  'Target Region',\n  'Output Language',\n];\n\nconst missingFields = requiredFields.filter((field) => {\n  const value = item[field];\n  return value === undefined || value === null || String(value).trim() === '';\n});\n\nif (missingFields.length > 0) {\n  return [\n    {\n      json: {\n        ...item,\n        validation_passed: false,\n        validation_error: `Missing required fields: ${missingFields.join(', ')}`,\n      },\n    },\n  ];\n}\n\nreturn [\n  {\n    json: {\n      ...item,\n      validation_passed: true,\n      validation_error: '',\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "0cd3f735-c084-47f3-9ca8-38cf15b61c98",
      "name": "Update row in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -208,
        -352
      ],
      "parameters": {
        "columns": {
          "value": {
            "Status": "processing",
            "row_number": "={{ $json.row_number }}"
          },
          "schema": [
            {
              "id": "Diagnosis",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Diagnosis",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Physician Plan",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Physician Plan",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Physician Message",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Physician Message",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Target Region",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Target Region",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Output Language",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Output Language",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Additional Notes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Additional Notes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Completed at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Completed at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Document URL",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Document URL",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Error Message",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Error Message",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "row_number"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1mZ-2TlSSvYFXlKPsCkQlIy7K5ekbRPYyftB2l-9D3v0/edit#gid=0",
          "cachedResultName": "Patient Education Requests"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1mZ-2TlSSvYFXlKPsCkQlIy7K5ekbRPYyftB2l-9D3v0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1mZ-2TlSSvYFXlKPsCkQlIy7K5ekbRPYyftB2l-9D3v0/edit?usp=drivesdk",
          "cachedResultName": "AI Personalized Patient Education Generator"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "d4cfd8b1-c6ac-458b-b4be-d9ad21a5ed9f",
      "name": "Prepare Workflow Context",
      "type": "n8n-nodes-base.set",
      "position": [
        -16,
        -352
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "c40a8de3-76fb-455e-a25b-7b8715aa6124",
              "name": "Diagnosis",
              "type": "string",
              "value": "={{ $('Get Pending Patient Education Request').item.json.Diagnosis }}"
            },
            {
              "id": "1aae4b01-589e-49e0-bfdc-c9d1eae7fa6b",
              "name": "Physician Plan",
              "type": "string",
              "value": "={{ $('Get Pending Patient Education Request').item.json['Physician Plan'] }}"
            },
            {
              "id": "6855d984-49d6-4997-89eb-67cbe71df41d",
              "name": "Physician Message",
              "type": "string",
              "value": "={{ $('Get Pending Patient Education Request').item.json['Physician Message'] }}"
            },
            {
              "id": "3f475834-a319-459b-b752-f618868367e7",
              "name": "Target Region",
              "type": "string",
              "value": "={{ $('Get Pending Patient Education Request').item.json['Target Region'] }}"
            },
            {
              "id": "1d0d5471-917d-4786-8e6d-d446db42128a",
              "name": "Output Language",
              "type": "string",
              "value": "={{ $('Get Pending Patient Education Request').item.json['Output Language'] }}"
            },
            {
              "id": "ec4315cf-af9d-44d8-b40d-bfb163423e56",
              "name": "Additional Notes",
              "type": "string",
              "value": "={{ $('Get Pending Patient Education Request').item.json['Additional Notes'] }}"
            },
            {
              "id": "f323626d-ae42-47dc-91d0-b071c2a44b0e",
              "name": "Status",
              "type": "string",
              "value": "processing"
            },
            {
              "id": "76af2430-8013-47a9-bec7-8ebf31d65e49",
              "name": "row_number",
              "type": "number",
              "value": "={{ $('Get Pending Patient Education Request').item.json.row_number }}"
            },
            {
              "id": "13761ff0-9224-405e-91ce-240573462311",
              "name": "Search Purpose",
              "type": "string",
              "value": "patient_education"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "bda72680-b467-4223-bec0-15508d567be8",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        -832,
        464
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "1824094b-04f2-49fb-8678-60154557a98d",
      "name": "Search Trusted Medical Sources",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -832,
        16
      ],
      "parameters": {
        "url": "https://api.search.brave.com/res/v1/web/search",
        "options": {},
        "sendQuery": true,
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ $json.Diagnosis + ' guideline ' + $json['Target Region'] }}"
            },
            {
              "name": "count",
              "value": "5"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "daaec9de-c68f-40f4-88f8-d4459fd0c9b9",
      "name": "Evaluate Search Results",
      "type": "n8n-nodes-base.code",
      "position": [
        -624,
        16
      ],
      "parameters": {
        "jsCode": "const MAX_RESULTS = 5;\nconst RAW_RESULT_LIMIT = 10;\n\n// Brave Search\u306e\u30ec\u30b9\u30dd\u30f3\u30b9\nconst input = $input.first().json;\n\n// \u60a3\u8005\u8aac\u660e\u30ef\u30fc\u30af\u30d5\u30ed\u30fc\u306e\u5165\u529b\u60c5\u5831\nconst context = $('Prepare Workflow Context').item.json;\n\n// \u6700\u521d\u304b\u30895\u4ef6\u306b\u9650\u5b9a\u305b\u305a\u3001\u5019\u88dc\u3092\u5c11\u3057\u5e83\u3081\u306b\u78ba\u8a8d\nconst rawResults = (input.web?.results ?? []).slice(\n  0,\n  RAW_RESULT_LIMIT\n);\n\n// Brave Search\u306e\u691c\u7d22\u60c5\u5831\nconst query = input.query?.original ?? '';\n\nconst restrictedDomains =\n  input.query?.search_operators?.sites ?? [];\n\n// \u660e\u3089\u304b\u306a\u30ce\u30a4\u30ba\u306e\u307f\u9664\u5916\nconst excludedPatterns = [\n  /guide to authors/i,\n  /instructions for authors/i,\n  /\u6295\u7a3f\u898f\u5b9a/i,\n  /\u5b66\u8853\u59d4\u54e1\u4f1a/i,\n  /\u63a1\u7528\u60c5\u5831/i,\n  /\u6c42\u4eba/i,\n  /\u30bb\u30df\u30ca\u30fc/i,\n  /\u5b66\u4f1a\u958b\u50ac/i,\n];\n\nconst normalizedResults = rawResults\n  .map((result, index) => {\n    const title = result.title ?? '';\n    const description = result.description ?? '';\n    const url = result.url ?? '';\n\n    let hostname =\n      result.meta_url?.hostname ??\n      result.profile?.long_name ??\n      '';\n\n    hostname = String(hostname)\n      .trim()\n      .toLowerCase()\n      .replace(/^www\\./, '');\n\n    const isPdf =\n      result.content_type === 'pdf' ||\n      /\\.pdf(?:$|\\?)/i.test(url);\n\n    const excluded = excludedPatterns.some(\n      (pattern) =>\n        pattern.test(title) ||\n        pattern.test(description) ||\n        pattern.test(url)\n    );\n\n    return {\n      original_rank: index + 1,\n      title,\n      url,\n      description,\n      hostname,\n      language: result.language ?? '',\n      content_type: isPdf ? 'pdf' : 'html',\n      excluded,\n      exclusion_reason: excluded\n        ? 'Likely administrative, promotional, or non-clinical content'\n        : '',\n    };\n  })\n  .filter((result) => {\n    const domainAllowed =\n      restrictedDomains.length === 0 ||\n      restrictedDomains.some((domain) => {\n        const normalizedDomain = String(domain)\n          .trim()\n          .toLowerCase()\n          .replace(/^www\\./, '');\n\n        return (\n          result.hostname === normalizedDomain ||\n          result.hostname.endsWith(\n            `.${normalizedDomain}`\n          )\n        );\n      });\n\n    return domainAllowed && !result.excluded;\n  })\n  .slice(0, MAX_RESULTS)\n  .map((result, index) => ({\n    ...result,\n    rank: index + 1,\n  }));\n\nreturn [\n  {\n    json: {\n      // \u533b\u5e2b\u304c\u5165\u529b\u3057\u305f\u60c5\u5831\u3092\u4fdd\u6301\n      Diagnosis: context.Diagnosis,\n      'Physician Plan': context['Physician Plan'],\n      'Physician Message':\n        context['Physician Message'],\n      'Target Region': context['Target Region'],\n      'Output Language':\n        context['Output Language'],\n      'Additional Notes':\n        context['Additional Notes'] ?? '',\n      Status: context.Status,\n      row_number: context.row_number,\n      'Search Purpose': context['Search Purpose'],\n\n      // \u691c\u7d22\u7d50\u679c\n      search_query: query,\n      restricted_domains: restrictedDomains,\n      total_raw_results:\n        input.web?.results?.length ?? 0,\n      analyzed_result_count: rawResults.length,\n      total_filtered_results:\n        normalizedResults.length,\n      search_results: normalizedResults,\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "07a966e9-1e56-4e81-beac-0036936c38a0",
      "name": "Select Medical Source",
      "type": "n8n-nodes-base.code",
      "position": [
        -432,
        16
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\nconst candidates = input.search_results ?? [];\n\n// \u5019\u88dc\u304c\u306a\u3044\u5834\u5408\nif (candidates.length === 0) {\n  return [\n    {\n      json: {\n        ...input,\n        evaluated_candidates: [],\n        guideline_selection: {\n          found: false,\n          reason:\n            \"No usable guideline or patient education source was found.\",\n          selected_title: \"\",\n          selected_url: \"\",\n          publication_year: null,\n          content_type: \"\",\n          hostname: \"\",\n          document_type: \"\",\n          search_rank: null,\n          selection_score: null,\n          selection_reasons: [],\n        },\n      },\n    },\n  ];\n}\n\n/**\n * \u5019\u88dc\u30da\u30fc\u30b8\u3092\u8a55\u4fa1\u3059\u308b\n *\n * \u512a\u5148\u9806\u4f4d\uff1a\n * 1. \u6b63\u5f0f\u306a\u8a3a\u7642\u30ac\u30a4\u30c9\u30e9\u30a4\u30f3\n * 2. \u516c\u7684\u6a5f\u95a2\u30fb\u5c02\u9580\u5b66\u4f1a\u306e\u60a3\u8005\u5411\u3051\u60c5\u5831\n * 3. \u5927\u5b66\u75c5\u9662\u30fb\u516c\u7684\u75c5\u9662\u306e\u60a3\u8005\u5411\u3051\u60c5\u5831\n * 4. \u305d\u306e\u4ed6\u306e\u81e8\u5e8a\u60c5\u5831\n * 5. \u7814\u7a76\u8ab2\u984c\u30fb\u7814\u7a76\u6210\u679c\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\n */\nfunction evaluateCandidate(candidate) {\n  const title = String(candidate.title ?? \"\");\n  const description = String(candidate.description ?? \"\");\n  const hostname = String(candidate.hostname ?? \"\")\n    .trim()\n    .toLowerCase()\n    .replace(/^www\\./, \"\");\n\n  const combinedText = `${title} ${description}`;\n\n  let score = 0;\n  const reasons = [];\n\n  /*\n   * \u6587\u66f8\u30bf\u30a4\u30d7\u306e\u4e8b\u524d\u5224\u5b9a\n   */\n\n  const looksLikeResearchRecord =\n    /\u7814\u7a76\u6210\u679c\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9|\u7814\u7a76\u8ab2\u984c|\u7814\u7a76\u73ed|\u7814\u7a76\u8a08\u753b|\u79d1\u5b66\u7814\u7a76\u8cbb|research project|research database/i.test(\n      combinedText\n    );\n\n  const looksLikeFormalGuideline =\n    /\u8a3a\u7642\u30ac\u30a4\u30c9\u30e9\u30a4\u30f3|\u6cbb\u7642\u30ac\u30a4\u30c9\u30e9\u30a4\u30f3|clinical practice guideline|guidelines? for/i.test(\n      title\n    ) && !looksLikeResearchRecord;\n\n  const looksLikePatientEducation =\n    /\u60a3\u8005\u306e\u7686\u69d8|\u60a3\u8005\u3055\u3093|\u60a3\u8005\u5411\u3051|\u4e00\u822c\u306e\u65b9|\u75c5\u6c17\u306b\u3064\u3044\u3066|\u5faa\u74b0\u5668\u75c5\u306b\u3064\u3044\u3066\u77e5\u308b|patient education|information for patients/i.test(\n      combinedText\n    );\n\n  const containsClinicalGuidance =\n    /\u63a8\u5968|\u8a3a\u7642\u6307\u91dd|\u6cbb\u7642\u6307\u91dd|\u6cbb\u7642\u65b9\u91dd|\u7d4c\u904e\u89b3\u5bdf|recommendation|clinical guidance/i.test(\n      combinedText\n    );\n\n  /*\n   * 1. \u60c5\u5831\u6e90\u306e\u4fe1\u983c\u6027\n   */\n\n  if (\n    hostname.endsWith(\".go.jp\") ||\n    hostname.includes(\".go.jp\")\n  ) {\n    score += 50;\n    reasons.push(\n      \"Japanese government or public institution domain\"\n    );\n  }\n\n  if (\n    hostname.endsWith(\".ac.jp\") ||\n    hostname.includes(\".ac.jp\")\n  ) {\n    score += 35;\n    reasons.push(\"Japanese academic institution domain\");\n  }\n\n  if (\n    hostname.endsWith(\".or.jp\") ||\n    hostname.includes(\".or.jp\")\n  ) {\n    score += 25;\n    reasons.push(\n      \"Japanese professional or nonprofit organization domain\"\n    );\n  }\n\n  /*\n   * 2. \u6b63\u5f0f\u30ac\u30a4\u30c9\u30e9\u30a4\u30f3\u306e\u53ef\u80fd\u6027\n   */\n\n  if (looksLikeFormalGuideline) {\n    score += 40;\n    reasons.push(\"Likely formal clinical guideline\");\n  } else if (\n    /\u30ac\u30a4\u30c9\u30e9\u30a4\u30f3|guideline/i.test(combinedText)\n  ) {\n    score += 10;\n    reasons.push(\n      \"Guideline mentioned, but the document may not be the guideline itself\"\n    );\n  }\n\n  /*\n   * 3. \u60a3\u8005\u8aac\u660e\u3078\u306e\u9069\u6027\n   */\n\n  if (looksLikePatientEducation) {\n    score += 35;\n    reasons.push(\n      \"Official or institutional patient education content\"\n    );\n  }\n\n  /*\n   * 4. \u81e8\u5e8a\u7684\u306a\u8aac\u660e\u30fb\u6307\u91dd\u3092\u542b\u3080\u53ef\u80fd\u6027\n   */\n\n  if (containsClinicalGuidance) {\n    score += 15;\n    reasons.push(\n      \"Contains clinical guidance or recommendation language\"\n    );\n  }\n\n  /*\n   * 5. PDF\u6587\u66f8\n   */\n\n  if (candidate.content_type === \"pdf\") {\n    score += 10;\n    reasons.push(\n      \"PDF document may contain formal guidance\"\n    );\n  }\n\n  /*\n   * 6. \u5546\u7528\u60c5\u5831\u6e90\n   */\n\n  if (hostname === \"clinicalsup.jp\") {\n    score -= 20;\n    reasons.push(\"Commercial clinical information source\");\n  }\n\n  /*\n   * 7. \u7814\u7a76\u8ab2\u984c\u30fb\u7814\u7a76\u6210\u679c\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\n   *\n   * \u300c\u30ac\u30a4\u30c9\u30e9\u30a4\u30f3\u300d\u3068\u3044\u3046\u8a9e\u304c\u542b\u307e\u308c\u3066\u3044\u3066\u3082\u3001\n   * \u6b63\u5f0f\u30ac\u30a4\u30c9\u30e9\u30a4\u30f3\u672c\u6587\u3067\u306f\u306a\u3044\u53ef\u80fd\u6027\u304c\u9ad8\u3044\u305f\u3081\u5927\u5e45\u306b\u6e1b\u70b9\n   */\n\n  if (looksLikeResearchRecord) {\n    score -= 70;\n    reasons.push(\n      \"Research project or research database record, not a current guideline or patient education document\"\n    );\n  }\n\n  /*\n   * \u691c\u7d22\u30b9\u30cb\u30da\u30c3\u30c8\u5185\u306e\u5e74\u306f\u3001\n   * \u767a\u884c\u5e74\u30fb\u7814\u7a76\u958b\u59cb\u5e74\u30fb\u75c7\u4f8b\u5e74\u306a\u3069\u3092\u533a\u5225\u3067\u304d\u306a\u3044\u305f\u3081\u3001\n   * publication_year\u3068\u3057\u3066\u81ea\u52d5\u63a1\u7528\u3057\u306a\u3044\n   */\n\n  const publicationYear = null;\n\n  return {\n    ...candidate,\n    publication_year: publicationYear,\n    selection_score: score,\n    selection_reasons: reasons,\n    source_classification: {\n      formal_guideline_candidate: looksLikeFormalGuideline,\n      patient_education_candidate: looksLikePatientEducation,\n      research_record: looksLikeResearchRecord,\n      contains_clinical_guidance: containsClinicalGuidance,\n    },\n  };\n}\n\n// \u5168\u5019\u88dc\u3092\u8a55\u4fa1\u3057\u3001\u70b9\u6570\u9806\u306b\u4e26\u3079\u308b\nconst evaluatedCandidates = candidates\n  .map(evaluateCandidate)\n  .sort((a, b) => {\n    if (b.selection_score !== a.selection_score) {\n      return b.selection_score - a.selection_score;\n    }\n\n    // \u540c\u70b9\u306e\u5834\u5408\u306fBrave Search\u306e\u9806\u4f4d\u3092\u512a\u5148\n    return (a.rank ?? 999) - (b.rank ?? 999);\n  });\n\nconst selectedSource = evaluatedCandidates[0];\n\n/*\n * \u9078\u629e\u3057\u305f\u6587\u66f8\u306e\u7a2e\u985e\u3092\u8a2d\u5b9a\n */\n\nlet documentType = \"supporting_clinical_source\";\n\nif (\n  selectedSource.source_classification\n    ?.formal_guideline_candidate\n) {\n  documentType = \"formal_guideline_candidate\";\n} else if (\n  selectedSource.source_classification\n    ?.patient_education_candidate\n) {\n  documentType =\n    \"official_patient_education_source\";\n} else if (\n  selectedSource.source_classification\n    ?.research_record\n) {\n  documentType = \"research_record\";\n} else if (\n  selectedSource.source_classification\n    ?.contains_clinical_guidance\n) {\n  documentType = \"clinical_guidance_source\";\n}\n\nreturn [\n  {\n    json: {\n      ...input,\n\n      evaluated_candidates: evaluatedCandidates,\n\n      guideline_selection: {\n        found: true,\n        reason:\n          \"Selected using source authority, formal guideline likelihood, patient education suitability, and document type.\",\n        selected_title: selectedSource.title ?? \"\",\n        selected_url: selectedSource.url ?? \"\",\n        publication_year:\n          selectedSource.publication_year ?? null,\n        content_type:\n          selectedSource.content_type ?? \"\",\n        hostname:\n          selectedSource.hostname ?? \"\",\n        document_type: documentType,\n        search_rank:\n          selectedSource.rank ?? null,\n        selection_score:\n          selectedSource.selection_score ?? null,\n        selection_reasons:\n          selectedSource.selection_reasons ?? [],\n      },\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "1579d2fc-03a9-46ae-b6a9-c317dd0e0f25",
      "name": "Download Source Page",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -240,
        16
      ],
      "parameters": {
        "url": "={{ $json.guideline_selection.selected_url }}",
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true,
              "responseFormat": "text"
            }
          },
          "allowUnauthorizedCerts": true
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "e460f968-7cb6-4226-b956-8db5d1f66b40",
      "name": "Extract Medical Content",
      "type": "n8n-nodes-base.code",
      "position": [
        -32,
        16
      ],
      "parameters": {
        "jsCode": "const response = $input.first().json;\n\n/*\n * \u524d\u6bb5\u306e\u9078\u629e\u7d50\u679c\u3092\u53d6\u5f97\n *\n * \u5b9f\u969b\u306e\u30ce\u30fc\u30c9\u540d\u3068\u5b8c\u5168\u306b\u4e00\u81f4\u3055\u305b\u3066\u304f\u3060\u3055\u3044\u3002\n */\nconst previous =\n  $(\"Select Medical Source\").first().json;\n\nconst html = String(response.data ?? \"\");\n\nconst statusCode =\n  response.statusCode ??\n  response.status_code ??\n  null;\n\nconst responseHeaders =\n  response.headers ?? {};\n\nconst responseContentType = String(\n  responseHeaders[\"content-type\"] ??\n  responseHeaders[\"Content-Type\"] ??\n  \"\"\n);\n\n/*\n * HTML\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u30c7\u30b3\u30fc\u30c9\n */\nfunction decodeHtml(text) {\n  return String(text ?? \"\")\n    .replace(/&nbsp;/gi, \" \")\n    .replace(/&#160;/gi, \" \")\n    .replace(/&amp;/gi, \"&\")\n    .replace(/&quot;/gi, '\"')\n    .replace(/&#34;/gi, '\"')\n    .replace(/&#39;/gi, \"'\")\n    .replace(/&#x27;/gi, \"'\")\n    .replace(/&apos;/gi, \"'\")\n    .replace(/&lt;/gi, \"<\")\n    .replace(/&gt;/gi, \">\");\n}\n\n/*\n * HTML\u304b\u3089\u8868\u793a\u7528\u30c6\u30ad\u30b9\u30c8\u3092\u751f\u6210\n */\nfunction stripHtml(value) {\n  return decodeHtml(\n    String(value ?? \"\")\n      .replace(\n        /<script\\b[^>]*>[\\s\\S]*?<\\/script>/gi,\n        \" \"\n      )\n      .replace(\n        /<style\\b[^>]*>[\\s\\S]*?<\\/style>/gi,\n        \" \"\n      )\n      .replace(\n        /<noscript\\b[^>]*>[\\s\\S]*?<\\/noscript>/gi,\n        \" \"\n      )\n      .replace(\n        /<svg\\b[^>]*>[\\s\\S]*?<\\/svg>/gi,\n        \" \"\n      )\n      .replace(\n        /<form\\b[^>]*>[\\s\\S]*?<\\/form>/gi,\n        \" \"\n      )\n      .replace(/<!--[\\s\\S]*?-->/g, \" \")\n      .replace(/<br\\s*\\/?>/gi, \"\\n\")\n      .replace(/<\\/p>/gi, \"\\n\")\n      .replace(/<\\/div>/gi, \"\\n\")\n      .replace(/<\\/section>/gi, \"\\n\")\n      .replace(/<\\/article>/gi, \"\\n\")\n      .replace(/<\\/li>/gi, \"\\n\")\n      .replace(/<\\/tr>/gi, \"\\n\")\n      .replace(/<\\/h[1-6]>/gi, \"\\n\")\n      .replace(/<[^>]+>/g, \" \")\n  )\n    .replace(/\\r/g, \"\")\n    .replace(/[ \\t]+/g, \" \")\n    .replace(/ *\\n */g, \"\\n\")\n    .replace(/\\n{3,}/g, \"\\n\\n\")\n    .trim();\n}\n\n/*\n * \u6b63\u898f\u8868\u73fe\u306e\u6700\u521d\u306e\u4e00\u81f4\u3092\u53d6\u5f97\n */\nfunction firstMatch(pattern, source) {\n  const match = String(source ?? \"\").match(pattern);\n\n  if (!match?.[1]) {\n    return \"\";\n  }\n\n  return stripHtml(match[1]);\n}\n\n/*\n * \u30da\u30fc\u30b8\u30bf\u30a4\u30c8\u30eb\n */\nconst pageTitle =\n  firstMatch(\n    /<title[^>]*>([\\s\\S]*?)<\\/title>/i,\n    html\n  ) ||\n  previous.guideline_selection?.selected_title ||\n  \"\";\n\n/*\n * meta description\n */\nconst metaDescription =\n  firstMatch(\n    /<meta[^>]+name=[\"']description[\"'][^>]+content=[\"']([\\s\\S]*?)[\"'][^>]*>/i,\n    html\n  ) ||\n  firstMatch(\n    /<meta[^>]+content=[\"']([\\s\\S]*?)[\"'][^>]+name=[\"']description[\"'][^>]*>/i,\n    html\n  ) ||\n  \"\";\n\n/*\n * \u30da\u30fc\u30b8\u5185\u306e\u4e3b\u8981\u90e8\u5206\u3092\u512a\u5148\u3057\u3066\u62bd\u51fa\n *\n * article\u3001main\u3001body\u306e\u9806\u306b\u8a66\u3059\n */\nconst articleHtmlMatch = html.match(\n  /<article\\b[^>]*>([\\s\\S]*?)<\\/article>/i\n);\n\nconst mainHtmlMatch = html.match(\n  /<main\\b[^>]*>([\\s\\S]*?)<\\/main>/i\n);\n\nconst bodyHtmlMatch = html.match(\n  /<body\\b[^>]*>([\\s\\S]*?)<\\/body>/i\n);\n\nconst selectedContentHtml =\n  articleHtmlMatch?.[1] ??\n  mainHtmlMatch?.[1] ??\n  bodyHtmlMatch?.[1] ??\n  html;\n\n/*\n * \u53ef\u8996\u30c6\u30ad\u30b9\u30c8\n */\nconst fullVisibleText = stripHtml(selectedContentHtml);\n\n/*\n * \u660e\u3089\u304b\u306a\u30e1\u30cb\u30e5\u30fc\u30fb\u5171\u901a\u8868\u793a\u3092\u7c21\u6613\u9664\u53bb\n *\n * \u5b8c\u5168\u306a\u672c\u6587\u62bd\u51fa\u3067\u306f\u306a\u304f\u3001\n * AI\u3078\u6e21\u3059\u524d\u306e\u30ce\u30a4\u30ba\u524a\u6e1b\u3092\u76ee\u7684\u3068\u3059\u308b\n */\nconst boilerplatePatterns = [\n  /^\u30db\u30fc\u30e0$/i,\n  /^\u30c8\u30c3\u30d7\u30da\u30fc\u30b8$/i,\n  /^\u30b5\u30a4\u30c8\u30de\u30c3\u30d7$/i,\n  /^\u304a\u554f\u3044\u5408\u308f\u305b$/i,\n  /^\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u30dd\u30ea\u30b7\u30fc$/i,\n  /^\u30a2\u30af\u30bb\u30b7\u30d3\u30ea\u30c6\u30a3$/i,\n  /^\u672c\u6587\u3078\u79fb\u52d5$/i,\n  /^\u30e1\u30cb\u30e5\u30fc$/i,\n  /^\u691c\u7d22$/i,\n  /^\u5370\u5237$/i,\n  /^\u30da\u30fc\u30b8\u306e\u5148\u982d\u3078$/i,\n  /^Copyright\\b/i,\n  /^\u00a9\\s*/i,\n];\n\nconst cleanedLines = fullVisibleText\n  .split(\"\\n\")\n  .map((line) => line.trim())\n  .filter(Boolean)\n  .filter((line) => {\n    return !boilerplatePatterns.some((pattern) =>\n      pattern.test(line)\n    );\n  });\n\nconst cleanedText = cleanedLines\n  .join(\"\\n\")\n  .replace(/\\n{3,}/g, \"\\n\\n\")\n  .trim();\n\n/*\n * \u898b\u51fa\u3057\u3092\u62bd\u51fa\n */\nconst headingMatches = [\n  ...selectedContentHtml.matchAll(\n    /<h([1-6])[^>]*>([\\s\\S]*?)<\\/h\\1>/gi\n  ),\n];\n\nconst headings = headingMatches\n  .map((match) => ({\n    level: Number(match[1]),\n    text: stripHtml(match[2]),\n  }))\n  .filter((item) => item.text);\n\n/*\n * \u30da\u30fc\u30b8\u5185\u30ea\u30f3\u30af\u3092\u62bd\u51fa\n */\nconst linkMatches = [\n  ...html.matchAll(\n    /<a[^>]+href=[\"']([^\"']+)[\"'][^>]*>([\\s\\S]*?)<\\/a>/gi\n  ),\n];\n\nconst baseUrl =\n  previous.guideline_selection?.selected_url ?? \"\";\n\nconst links = linkMatches\n  .map((match) => {\n    const href = String(match[1] ?? \"\").trim();\n    const text = stripHtml(match[2]);\n\n    let absoluteUrl = href;\n\n    try {\n      absoluteUrl = new URL(\n        href,\n        baseUrl\n      ).toString();\n    } catch {\n      absoluteUrl = href;\n    }\n\n    return {\n      text,\n      url: absoluteUrl,\n    };\n  })\n  .filter((link) => {\n    return (\n      link.url &&\n      !link.url.startsWith(\"javascript:\") &&\n      !link.url.startsWith(\"mailto:\") &&\n      !link.url.startsWith(\"tel:\")\n    );\n  });\n\n/*\n * PDF\u30ea\u30f3\u30af\u3092\u62bd\u51fa\n */\nconst excludedPdfPatterns = [\n  /errata/i,\n  /\u6b63\u8aa4\u8868/i,\n  /coi/i,\n  /\u5229\u76ca\u76f8\u53cd/i,\n  /\u8ee2\u8f09/i,\n  /permission/i,\n  /\u7533\u8acb\u66f8/i,\n  /\u69d8\u5f0f/i,\n];\n\nconst pdfLinks = links.filter((link) => {\n  const combined =\n    `${link.text} ${link.url}`;\n\n  const isPdf =\n    /\\.pdf(?:$|[?#])/i.test(link.url);\n\n  const excluded =\n    excludedPdfPatterns.some((pattern) =>\n      pattern.test(combined)\n    );\n\n  return isPdf && !excluded;\n});\n\n/*\n * \u91cd\u8907\u30ea\u30f3\u30af\u3092\u9664\u53bb\n */\nconst uniquePdfLinks = [\n  ...new Map(\n    pdfLinks.map((link) => [\n      link.url,\n      link,\n    ])\n  ).values(),\n];\n\n/*\n * AI\u3078\u6e21\u3059\u672c\u6587\u306e\u6587\u5b57\u6570\u5236\u9650\n *\n * \u9577\u3059\u304e\u308bHTML\u3092\u305d\u306e\u307e\u307eLLM\u3078\u6e21\u3059\u3053\u3068\u3092\u907f\u3051\u308b\n */\nconst MAX_SOURCE_TEXT_LENGTH = 12000;\n\nconst sourceText =\n  cleanedText.slice(\n    0,\n    MAX_SOURCE_TEXT_LENGTH\n  );\n\nconst wasTruncated =\n  cleanedText.length >\n  MAX_SOURCE_TEXT_LENGTH;\n\n/*\n * fetch\u6210\u529f\u5224\u5b9a\n *\n * Never Error\u304cON\u306e\u305f\u3081\u3001\n * 2xx\u4ee5\u5916\u3067\u3082\u30ce\u30fc\u30c9\u81ea\u4f53\u306f\u6210\u529f\u3059\u308b\u53ef\u80fd\u6027\u304c\u3042\u308b\n */\nconst fetchSuccess =\n  statusCode !== null\n    ? statusCode >= 200 &&\n      statusCode < 300\n    : html.length > 0;\n\n/*\n * \u672c\u6587\u3068\u3057\u3066\u5229\u7528\u3067\u304d\u308b\u6700\u4f4e\u6761\u4ef6\n */\nconst minimumUsefulLength = 300;\n\nconst contentUsable =\n  fetchSuccess &&\n  sourceText.length >= minimumUsefulLength;\n\nlet accessStatus =\n  \"content_available\";\n\nif (!fetchSuccess) {\n  accessStatus = \"fetch_failed\";\n} else if (!contentUsable) {\n  accessStatus =\n    \"insufficient_public_content\";\n} else if (wasTruncated) {\n  accessStatus =\n    \"content_available_truncated\";\n}\n\n/*\n * \u51fa\u529b\n */\nreturn [\n  {\n    json: {\n      ...previous,\n\n      source_content: {\n        fetch_success: fetchSuccess,\n        content_usable: contentUsable,\n\n        status_code: statusCode,\n        response_content_type:\n          responseContentType,\n\n        source_url: baseUrl,\n        page_title: pageTitle,\n        meta_description:\n          metaDescription,\n\n        selected_source_type:\n          previous.guideline_selection\n            ?.document_type ?? \"\",\n\n        organization_hostname:\n          previous.guideline_selection\n            ?.hostname ?? \"\",\n\n        access_status: accessStatus,\n\n        heading_count:\n          headings.length,\n\n        headings:\n          headings.slice(0, 30),\n\n        supporting_pdf_available:\n          uniquePdfLinks.length > 0,\n\n        supporting_pdf_links:\n          uniquePdfLinks.slice(0, 10),\n\n        source_text:\n          sourceText,\n\n        source_text_length:\n          sourceText.length,\n\n        original_text_length:\n          cleanedText.length,\n\n        text_truncated:\n          wasTruncated,\n\n        extraction_method:\n          articleHtmlMatch\n            ? \"article\"\n            : mainHtmlMatch\n              ? \"main\"\n              : bodyHtmlMatch\n                ? \"body\"\n                : \"full_html\",\n      },\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "174d1548-8299-4f64-b058-bdbf17db273a",
      "name": "Generate Patient Education",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -832,
        304
      ],
      "parameters": {
        "text": "=System Prompt\nYou are an expert medical patient education writer.\n\nYour task is to create a personalized patient education document using ONLY:\n\n\u2022 Physician Diagnosis\n\u2022 Physician Treatment Plan\n\u2022 Physician Message\n\u2022 Additional Notes\n\u2022 Trusted Medical Source supplied in the workflow\n\nThe physician's diagnosis, treatment plan, and instructions are authoritative.\n\nYour role is NOT to diagnose or recommend treatment.\n\nYour role is to explain the physician's decisions in language that patients and their families can easily understand.\n\nAlways write in a calm, respectful, professional, and naturally reassuring tone.\n\nAvoid unnecessary medical terminology.\n\nWhen a medical term is necessary, explain it using plain language the first time it appears.\n\nNever:\n\n\u2022 change the physician's diagnosis\n\u2022 change the physician's treatment plan\n\u2022 recommend additional treatments\n\u2022 recommend additional tests\n\u2022 recommend different follow-up intervals\n\u2022 introduce numerical thresholds not provided by the physician\n\u2022 infer disease severity, prognosis, risk, lesion size, lesion location, or lesion morphology\n\u2022 speculate\n\u2022 provide legal or financial advice\n\nUse the trusted medical source ONLY for general medical background that helps explain the physician's diagnosis and treatment plan.\n\nDo not create new patient-specific conclusions from the source.\n\nIf information in the source is not clearly applicable to this patient, simply omit it.\n\nFollow any communication preferences contained in Additional Notes.\n\nAlways generate the body text in the requested Output Language.\n\nKeep all Markdown headings exactly as written in English.\n\nDo not translate, rename, remove, duplicate, or reorder any heading.\n\nOnly the content beneath the headings should be written in the requested Output Language.\n\nDo not invent references.\n\nUse only the supplied trusted source.\n\nThe final document must be suitable for printing and handing directly to a patient or family member.\nUser Prompt\nGenerate a personalized patient education document using the information below.\n\n==================================================\nPatient Information\n==================================================\n\nDiagnosis:\n{{ $json.Diagnosis }}\n\nPhysician Treatment Plan:\n{{ $json[\"Physician Plan\"] }}\n\nPhysician Message:\n{{ $json[\"Physician Message\"] }}\n\nAdditional Notes:\n{{ $json[\"Additional Notes\"] }}\n\nTarget Region:\n{{ $json[\"Target Region\"] }}\n\nOutput Language:\n{{ $json[\"Output Language\"] }}\n\n==================================================\nTrusted Medical Information\n==================================================\n\nTitle:\n{{ $json.guideline_selection.selected_title }}\n\nURL:\n{{ $json.guideline_selection.selected_url }}\n\nMedical Source:\n\n{{ $json.source_content.source_text }}\n\n==================================================\nOutput Format\n==================================================\n\nUse the following Markdown headings exactly as written.\n\nDo not add separator lines.\n\nDo not add text before the title.\n\n# Patient Education Report\n\n## Diagnosis\n\nWrite the diagnosis exactly as supplied.\n\n## Your Doctor's Treatment Plan\n\nExplain the physician's treatment plan using patient-friendly language.\n\nPreserve all medical facts exactly.\n\nDo not add new tests, treatments, restrictions, permissions, or follow-up intervals.\n\n## A Message From Your Doctor\n\nRewrite the physician's message naturally while preserving its meaning.\n\nWhen writing Japanese, use \"\u62c5\u5f53\u533b\" instead of \"\u5148\u751f\".\n\nAvoid repeatedly using identical reassuring phrases.\n\n## Understanding Your Condition\n\nExplain:\n\n\u2022 what the condition is\n\n\u2022 what is generally known about it\n\n\u2022 what patients commonly experience\n\nUse only information supported by the supplied medical source.\n\nDo not introduce patient-specific conclusions.\n\n## Why This Plan Was Recommended\n\nExplain only the reason stated by the physician.\n\nThe trusted medical source may be used only as general background.\n\nDo not invent additional reasons.\n\n## What You Should Do\n\nInclude only actions explicitly stated in the physician's treatment plan or message.\n\nDo not introduce additional lifestyle advice.\n\n## When You Should Seek Medical Attention\n\nIf the supplied medical source clearly describes important warning signs requiring urgent medical attention, include them.\n\nDo not add warning signs from general medical knowledge.\n\nIf no warning signs are supported by the supplied source, advise the patient to ask the healthcare professional about symptoms requiring urgent evaluation.\n\nClearly distinguish emergency situations from routine follow-up.\n\n## Questions for Your Next Visit\n\nGenerate 3\u20135 useful questions directly related to the supplied physician information.\n\nDo not introduce unrelated lifestyle topics.\n\n## Information Source\n\nTitle:\n{{ $json.guideline_selection.selected_title }}\n\nURL:\n{{ $json.guideline_selection.selected_url }}\n\nAccessed:\n{{ $now.toFormat(\"yyyy-MM-dd\") }}\n\n## Important Note\n\nState naturally in the requested Output Language that:\n\n\u2022 this document summarizes the healthcare professional's explanation\n\n\u2022 it does not make a new diagnosis\n\n\u2022 it does not determine treatment\n\n\u2022 the healthcare professional's instructions always take priority",
        "batching": {},
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "1bef4f8f-4a18-4ede-937e-dc55d3581416",
      "name": "Split Document Pages",
      "type": "n8n-nodes-base.code",
      "position": [
        -528,
        432
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst report = String(input.text ?? \"\").trim();\n\nif (!report) {\n  throw new Error(\n    \"Generate Personalized Patient Education returned no text.\"\n  );\n}\n\nconst requiredHeadings = [\n  \"# Patient Education Report\",\n  \"## Diagnosis\",\n  \"## Your Doctor's Treatment Plan\",\n  \"## A Message From Your Doctor\",\n  \"## Understanding Your Condition\",\n  \"## Why This Plan Was Recommended\",\n  \"## What You Should Do\",\n  \"## When You Should Seek Medical Attention\",\n  \"## Questions for Your Next Visit\",\n  \"## Information Source\",\n  \"## Important Note\",\n];\n\nconst missingHeadings = requiredHeadings.filter(\n  (heading) => !report.includes(heading)\n);\n\nif (missingHeadings.length > 0) {\n  throw new Error(\n    `Missing required headings: ${missingHeadings.join(\", \")}`\n  );\n}\n\nfunction extractSection(startHeading, endHeading = null) {\n  const startIndex = report.indexOf(startHeading);\n\n  if (startIndex === -1) {\n    return \"\";\n  }\n\n  if (!endHeading) {\n    return report.slice(startIndex).trim();\n  }\n\n  const endIndex = report.indexOf(\n    endHeading,\n    startIndex + startHeading.length\n  );\n\n  if (endIndex === -1) {\n    return report.slice(startIndex).trim();\n  }\n\n  return report.slice(startIndex, endIndex).trim();\n}\n\n/*\n==================================================\nPage 1\n==================================================\n*/\n\nconst page1Sections = [\n  extractSection(\n    \"# Patient Education Report\",\n    \"## Diagnosis\"\n  ),\n\n  extractSection(\n    \"## Diagnosis\",\n    \"## Your Doctor's Treatment Plan\"\n  ),\n\n  extractSection(\n    \"## Your Doctor's Treatment Plan\",\n    \"## A Message From Your Doctor\"\n  ),\n\n  extractSection(\n    \"## A Message From Your Doctor\",\n    \"## Understanding Your Condition\"\n  ),\n\n  extractSection(\n    \"## Understanding Your Condition\",\n    \"## Why This Plan Was Recommended\"\n  ),\n];\n\n/*\n==================================================\nPage 2\n==================================================\n*/\n\nconst page2Sections = [\n  extractSection(\n    \"## Why This Plan Was Recommended\",\n    \"## What You Should Do\"\n  ),\n\n  extractSection(\n    \"## What You Should Do\",\n    \"## When You Should Seek Medical Attention\"\n  ),\n\n  extractSection(\n    \"## When You Should Seek Medical Attention\",\n    \"## Questions for Your Next Visit\"\n  ),\n\n  extractSection(\n    \"## Questions for Your Next Visit\",\n    \"## Information Source\"\n  ),\n\n  extractSection(\n    \"## Information Source\",\n    \"## Important Note\"\n  ),\n\n  extractSection(\n    \"## Important Note\"\n  ),\n];\n\nfunction joinSections(sections) {\n  return sections\n    .filter(Boolean)\n    .join(\"\\n\\n\")\n    .replace(/^##\\s+/gm, \"\")\n    .replace(/^#\\s+/gm, \"\")\n    .replace(/^\\*\\s+/gm, \"\u2022 \")\n    .replace(/\\n{3,}/g, \"\\n\\n\")\n    .trim();\n}\n\n/*\n * Google Docs\u3067\u30da\u30fc\u30b8\u4e0a\u90e8\u306b\u7d043\u884c\u5206\u306e\u4f59\u767d\u3092\u4f5c\u308b\n * 4\u3064\u306e\u6539\u884c\u3067\u3001\u898b\u305f\u76ee\u3068\u3057\u3066\u7d043\u884c\u5206\u306e\u7a7a\u767d\u306b\u306a\u308b\u60f3\u5b9a\n */\nconst PAGE_TOP_MARGIN = \"\\n\\n\\n\\n\";\n\nconst page1 =\n  PAGE_TOP_MARGIN +\n  joinSections(page1Sections);\n\nconst page2 =\n  PAGE_TOP_MARGIN +\n  joinSections(page2Sections);\n\nreturn [\n  {\n    json: {\n      ...input,\n\n      full_report: report,\n\n      page1,\n\n      page2,\n\n      page_count: 2,\n\n      split_success: true,\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "a652b7e3-81b0-4fe0-bf74-e7acc05c3b14",
      "name": "Create Google Document",
      "type": "n8n-nodes-base.googleDocs",
      "position": [
        -112,
        432
      ],
      "parameters": {
        "title": "=Patient Education - {{ $('Get Pending Patient Education Request').item.json.Diagnosis }} - {{ $now.toFormat(\"yyyy-MM-dd\") }} ",
        "folderId": "1kW57wbuZsgWvE0rydapKIeE0hWmLsLCn"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "37ffae4c-4fc1-41d9-bfd5-81f5c3699529",
      "name": "Insert Page 1",
      "type": "n8n-nodes-base.googleDocs",
      "position": [
        112,
        432
      ],
      "parameters": {
        "actionsUi": {
          "actionFields": [
            {
              "text": "={{ $('Split Document Pages').item.json.page1 }}",
              "action": "insert"
            }
          ]
        },
        "operation": "update",
        "documentURL": "={{ $json.id }}"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "83ef8576-4a5b-48c2-9dae-135c090169cd",
      "name": "Insert Page Break",
      "type": "n8n-nodes-base.googleDocs",
      "position": [
        320,
        432
      ],
      "parameters": {
        "actionsUi": {
          "actionFields": [
            {
              "action": "insert",
              "object": "pageBreak"
            }
          ]
        },
        "operation": "update",
        "documentURL": "={{ $('Create Google Document').item.json.id }}"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "9fc3a678-add3-472d-9782-f7c8d0554c86",
      "name": "Insert Page 2",
      "type": "n8n-nodes-base.googleDocs",
      "position": [
        528,
        432
      ],
      "parameters": {
        "actionsUi": {
          "actionFields": [
            {
              "text": "={{ $('Split Document Pages').item.json.page2 }}",
              "action": "insert"
            }
          ]
        },
        "operation": "update",
        "documentURL": "={{ $('Create Google Document').item.json.id }}"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "06f5b751-bf63-48dc-8bfc-c755feeedcbe",
      "name": "Mark Request Completed",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1120,
        432
      ],
      "parameters": {
        "columns": {
          "value": {
            "Status": "completed",
            "row_number": "={{ $('Get Pending Patient Education Request').item.json.row_number }}",
            "Completed at": "={{ $now.toISO() }}",
            "Document URL": "={{ \"https://docs.google.com/document/d/\" + $(\"Create Google Document\").item.json.id + \"/edit\" }}"
          },
          "schema": [
            {
              "id": "Diagnosis",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Diagnosis",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Physician Plan",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Physician Plan",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Physician Message",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Physician Message",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Target Region",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Target Region",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Output Language",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Output Language",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Additional Notes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Additional Notes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Completed at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Completed at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Document URL",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Document URL",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Error Message",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Error Message",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "row_number"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1mZ-2TlSSvYFXlKPsCkQlIy7K5ekbRPYyftB2l-9D3v0/edit#gid=0",
          "cachedResultName": "Patient Education Requests"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1mZ-2TlSSvYFXlKPsCkQlIy7K5ekbRPYyftB2l-9D3v0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1mZ-2TlSSvYFXlKPsCkQlIy7K5ekbRPYyftB2l-9D3v0/edit?usp=drivesdk",
          "cachedResultName": "AI Personalized Patient Education Generator"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "5f57e93a-69cb-418f-8712-9b1fd0533f25",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1232,
        -512
      ],
      "parameters": {
        "color": 7,
        "width": 1408,
        "height": 320,
        "content": "## INPUT\n\nRead a pending patient education request,\nvalidate required fields,\nand initialize the workflow."
      },
      "typeVersion": 1
    },
    {
      "id": "388a8427-0009-4ce4-9989-db44a740992d",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1232,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 1408,
        "height": 368,
        "content": "## MEDICAL SOURCE SEARCH\n\nSearch trusted medical sources,\nevaluate search results,\nselect the best patient education source,\nand extract relevant medical content."
      },
      "typeVersion": 1
    },
    {
      "id": "d872646a-3747-48bf-b572-9a68da756b01",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1232,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 912,
        "height": 400,
        "content": "## AI PATIENT EDUCATION\n\nGenerate a personalized patient education document\nusing the physician's diagnosis,\ntreatment plan,\nclinical message,\nand trusted medical content."
      },
      "typeVersion": 1
    },
    {
      "id": "4a20107c-51d7-429b-88c5-d3a803b11108",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -288,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 1024,
        "height": 400,
        "content": "## GOOGLE DOCS GENERATION\n\nCreate a printable Google Docs document,\ninsert each page,\nand apply page breaks."
      },
      "typeVersion": 1
    },
    {
      "id": "97ec2660-b4b1-4df7-b397-b5ce98724d7b",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        800,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 688,
        "height": 400,
        "content": "## COMPLETION\n\nUpdate request status,\ncompletion timestamp,\nand generated document URL."
      },
      "typeVersion": 1
    },
    {
      "id": "cc75bd36-7fc3-41cb-98bd-df5f2b52e81a",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1952,
        -512
      ],
      "parameters": {
        "width": 656,
        "height": 848,
        "content": "## # AI Personalized Patient Education Generator\n\nCreate personalized, print-ready patient education documents from a physician's diagnosis, treatment plan, message, and trusted medical information.\n\n## How it works\n\n1. Reads a pending patient education request from Google Sheets.\n2. Searches for relevant medical information using Brave Search.\n3. Evaluates the search results and selects a suitable medical source.\n4. Uses Google Gemini to create a patient-friendly explanation while keeping the physician's diagnosis and treatment plan authoritative.\n5. Creates a printable Google Docs document and updates the request status in Google Sheets.\n\n## Setup\n\n* [ ] Connect your Google Sheets account and select the request spreadsheet.\n* [ ] Add your Brave Search API credential.\n* [ ] Connect Google Gemini to the AI model node.\n* [ ] Connect Google Docs and select the destination Drive folder.\n* [ ] Add a request with the required patient information and set its status to `pending`.\n* [ ] Run the workflow and review the generated document before providing it to a patient.\n\n## Customization\n\nYou can adjust the target region, output language, source-selection rules, AI instructions, and Google Docs formatting for your organization.\n\n**Important:** This workflow supports patient communication. It does not replace clinical judgment. The physician's diagnosis, treatment plan, and individual instructions always take priority over AI-generated explanations.\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "callerPolicy": "workflowsFromSameOwner",
    "timeSavedMode": "fixed",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "4b2558bf-b7bf-42ba-97fd-df753b039b38",
  "connections": {
    "Insert Page 1": {
      "main": [
        [
          {
            "node": "Insert Page Break",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert Page 2": {
      "main": [
        [
          {
            "node": "Mark Request Completed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert Page Break": {
      "main": [
        [
          {
            "node": "Insert Page 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update row in sheet": {
      "main": [
        [
          {
            "node": "Prepare Workflow Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Source Page": {
      "main": [
        [
          {
            "node": "Extract Medical Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Document Pages": {
      "main": [
        [
          {
            "node": "Create Google Document",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select Medical Source": {
      "main": [
        [
          {
            "node": "Download Source Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Google Document": {
      "main": [
        [
          {
            "node": "Insert Page 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate Search Results": {
      "main": [
        [
          {
            "node": "Select Medical Source",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Medical Content": {
      "main": [
        [
          {
            "node": "Generate Patient Education",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Generate Patient Education",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Workflow Context": {
      "main": [
        [
          {
            "node": "Search Trusted Medical Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Patient Education": {
      "main": [
        [
          {
            "node": "Split Document Pages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Trusted Medical Sources": {
      "main": [
        [
          {
            "node": "Evaluate Search Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Patient Education Input": {
      "main": [
        [
          {
            "node": "Update row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Get Pending Patient Education Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Pending Patient Education Request": {
      "main": [
        [
          {
            "node": "Validate Patient Education Input",
            "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

Generate personalized, print-ready patient education documents from physician-provided diagnoses, treatment plans, and messages. The workflow finds relevant trusted medical information with Brave Search, uses Gemini to create patient-friendly explanations without overriding the…

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

Episode 11: AI shorts factory app. Uses httpRequest, googleSheets, lmChatOpenAi, lmChatOllama. Event-driven trigger; 96 nodes.

HTTP Request, Google Sheets, OpenAI Chat +15
AI & RAG

This workflow creates a multi-talented AI assistant named Simran that interacts with users via Telegram. It can handle text and voice messages, understand the user's intent, and perform various tasks.

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

Transform a single quote into a fully-rendered cinematic short video — with voice-over, visuals, and music — then publish it directly to TikTok, Instagram Reels, and YouTube Shorts. This isn’t just au

Agent, HTTP Request, Jwt +7
AI & RAG

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

HTTP Request, Google Sheets, OpenRouter Chat +5
AI & RAG

This workflow turns any URL sent to a Telegram bot into ready-to-publish social posts: Trigger: Telegram message (checks if it contains a URL). Fetch & parse: Downloads the page and extracts readable

Telegram Trigger, HTTP Request, Google Sheets +5