{
  "nodes": [
    {
      "id": "2a534e69-eb9d-4573-a57b-7e9222984945",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -400,
        -144
      ],
      "parameters": {
        "color": "#133FA4",
        "width": 420,
        "height": 732,
        "content": "# Extract Text From Google Drive Files\n\n**Who it's for:** Teams and individuals who need to automatically capture, structure, and log content from files dropped into a shared Google Drive folder.\n\n**What it does:** Monitors a Drive folder for new files, extracts text by file type, structures the result with NVIDIA NIM, logs it to Google Sheets, and sends a Telegram notification.\n\n**How it works:**\n1. Google Drive Trigger detects a new file\n2. File type is identified and routed (PDF, image, Google Docs, TXT, CSV)\n3. Text is extracted via the appropriate method for each type\n4. NVIDIA NIM structures the extracted text into a consistent schema\n5. Result is appended to a Google Sheets log\n6. Telegram notification confirms completion\n\n**Required setup:**\n- Google Drive OAuth2 credential\n- NVIDIA NIM API key (HTTP Header Auth)\n- Telegram bot credential\n- Google Sheets OAuth2 credential\n\nBuilt by Cordexa Technologies\nhttps://cordexa.tech\ncordexatech@gmail.com"
      },
      "typeVersion": 1
    },
    {
      "id": "3bea1e63-9a5d-46b7-a64c-871ccfcbe7b0",
      "name": "Section - Trigger and Input",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        64,
        -144
      ],
      "parameters": {
        "color": 7,
        "width": 606,
        "height": 740,
        "content": "## Step 1 \u2014 Config\n\nThis workflow watches a specific Google Drive folder every minute, normalizes file metadata, and routes each file by MIME type.\n\nBefore testing:\n- Replace `REPLACE_WITH_GOOGLE_DRIVE_FOLDER_ID` in `Google Drive Trigger` with your Drive folder ID.\n- Replace `REPLACE_WITH_TELEGRAM_CHAT_ID` in `Normalize Input` with your destination Telegram chat ID.\n\nSupported routes:\n- PDF\n- image\n- Google Doc\n- text file\n- CSV\n- unsupported fallback"
      },
      "typeVersion": 1
    },
    {
      "id": "cf8e5a70-a356-4928-a439-a07218e2fd80",
      "name": "Section - File Fetching",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        704,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 540,
        "height": 514,
        "content": "## File Fetching\nDownloads the file binary from Drive for local extraction (PDF, image, TXT, CSV) or fetches Google Doc content directly via the Docs API."
      },
      "typeVersion": 1
    },
    {
      "id": "c30bf8b9-954b-40b3-9afb-b3b06da0ccf4",
      "name": "Section - Extraction Branches",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1280,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 476,
        "height": 1056,
        "content": "## Extraction Branches\nExtracts raw text from each supported file type.\n- **PDF** \u2014 native Extract from File node\n- **TXT** \u2014 native Extract from File node\n- **CSV** \u2014 parsed to JSON rows\n- **Image** \u2014 NVIDIA Llama 3.2 vision model via HTTP\n- **Google Doc** \u2014 pre-fetched upstream, normalised here\n\nAll paths converge on a unified `content` field passed downstream."
      },
      "typeVersion": 1
    },
    {
      "id": "b401ae90-c106-4686-8a3c-e069984cd9e2",
      "name": "Section - Fallback Paths",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        704,
        640
      ],
      "parameters": {
        "color": 7,
        "width": 536,
        "height": 628,
        "content": "## Fallback Paths\nRaw text inputs bypass extraction entirely.\n\nUnsupported MIME types send a Telegram notification and stop \u2014 no Sheets row is written."
      },
      "typeVersion": 1
    },
    {
      "id": "9f515b82-5e33-4b1e-aade-e15632f5ac62",
      "name": "Section - AI Structuring",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1792,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 884,
        "height": 558,
        "content": "## AI Structuring with NVIDIA NIM\nSends extracted text to `nvidia/llama-3.3-nemotron-super-49b-v1.5` with a guided JSON schema. Returns: title, summary, category, language, key points, and confidence notes. Falls back gracefully if the model call fails (`continueOnFail: true`).\n\n\n**Setup:** Add your NVIDIA NIM API key as an HTTP Header Auth credential (`Authorization: Bearer YOUR_KEY`). Apply it to both NVIDIA HTTP Request nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "06c5660f-6546-4c81-9b74-424248fc3a62",
      "name": "Section - Log and Notify",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2720,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 510,
        "height": 736,
        "content": "## Step 3 \u2014 Delivery\n\nThis workflow delivers output in two places:\n\n- `Append Row in Sheet` writes a structured row to the `Extract_Log` tab\n- `Send Reply` sends a short Telegram confirmation message\n\nBefore going live:\n- Replace `REPLACE_WITH_GOOGLE_SHEET_ID` in `Append Row in Sheet`\n- Confirm the target sheet contains an `Extract_Log` tab\n- Confirm the sheet headers match the mapped fields in the node\n- Confirm Telegram replies go to the configured chat ID"
      },
      "typeVersion": 1
    },
    {
      "id": "f001b69a-0fee-413d-930a-b558021c9d02",
      "name": "Google Drive Trigger",
      "type": "n8n-nodes-base.googleDriveTrigger",
      "position": [
        144,
        304
      ],
      "parameters": {
        "event": "fileCreated",
        "options": {
          "fileType": "all"
        },
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "specificFolder",
        "folderToWatch": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_GOOGLE_DRIVE_FOLDER_ID"
        }
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ce0aacba-4dcf-4a66-a5f6-8b2a719dadd1",
      "name": "Normalize Input",
      "type": "n8n-nodes-base.code",
      "position": [
        336,
        304
      ],
      "parameters": {
        "jsCode": "const fileId = $json.id || $json.fileId || '';\nconst rawMimeType = ($json.mimeType || '').toString();\nconst fileName = ($json.name || $json.fileName || 'drive-file').toString();\nconst mimeType = rawMimeType;\nconst googleDocId = mimeType === 'application/vnd.google-apps.document' ? fileId : '';\nconst googleDocUrl = googleDocId ? `https://docs.google.com/document/d/${googleDocId}` : '';\n\nlet route = 'unsupported';\nif (mimeType === 'application/pdf') {\n  route = 'pdf';\n} else if (mimeType.startsWith('image/')) {\n  route = 'image';\n} else if (mimeType === 'application/vnd.google-apps.document') {\n  route = 'google_doc';\n} else if (mimeType === 'text/csv' || fileName.toLowerCase().endsWith('.csv')) {\n  route = 'csv';\n} else if (mimeType.startsWith('text/')) {\n  route = 'text_file';\n}\n\nreturn [{\n  json: {\n    route,\n    fileId,\n    rawText: '',\n    chatId: 'REPLACE_WITH_TELEGRAM_CHAT_ID',\n    messageId: '',\n    mimeType,\n    fileName,\n    binaryKey: '',\n    googleDocUrl,\n    googleDocId,\n    sourceType: 'google_drive_file',\n    runId: $execution.id\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9d174ac1-d0d4-4abf-9922-d3646a5107a4",
      "name": "Route Input",
      "type": "n8n-nodes-base.switch",
      "position": [
        576,
        304
      ],
      "parameters": {
        "rules": {
          "rules": [
            {
              "value2": "pdf",
              "outputKey": "pdf"
            },
            {
              "value2": "image",
              "outputKey": "image"
            },
            {
              "value2": "google_doc",
              "outputKey": "google_doc"
            },
            {
              "value2": "text_file",
              "outputKey": "text_file"
            },
            {
              "value2": "csv",
              "outputKey": "csv"
            },
            {
              "value2": "raw_text",
              "outputKey": "raw_text"
            }
          ]
        },
        "value1": "={{ $json.route }}",
        "dataType": "string"
      },
      "typeVersion": 2
    },
    {
      "id": "c6e2b331-1781-4f5f-9750-f3c3eba0fbfc",
      "name": "Download Drive File",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        832,
        240
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.fileId }}"
        },
        "options": {
          "fileName": "={{ $json.fileName }}"
        },
        "operation": "download"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "abddb5e5-6994-47f6-b766-65e388014cdf",
      "name": "Route Downloaded File",
      "type": "n8n-nodes-base.switch",
      "position": [
        1072,
        208
      ],
      "parameters": {
        "rules": {
          "rules": [
            {
              "value2": "pdf",
              "outputKey": "pdf"
            },
            {
              "value2": "image",
              "outputKey": "image"
            },
            {
              "value2": "text_file",
              "outputKey": "text_file"
            },
            {
              "value2": "csv",
              "outputKey": "csv"
            }
          ]
        },
        "value1": "={{ $('Normalize Input').item.json.route }}",
        "dataType": "string"
      },
      "typeVersion": 2
    },
    {
      "id": "9f0b9eaf-2ae4-4666-a35f-2f89b585a5cf",
      "name": "Get Google Doc",
      "type": "n8n-nodes-base.googleDocs",
      "position": [
        832,
        432
      ],
      "parameters": {
        "operation": "get",
        "documentURL": "={{ $('Normalize Input').first().json.googleDocId }}"
      },
      "typeVersion": 2
    },
    {
      "id": "96709023-7020-49c3-a2ce-ae6280e3b11b",
      "name": "Normalize Google Doc",
      "type": "n8n-nodes-base.code",
      "position": [
        1072,
        432
      ],
      "parameters": {
        "jsCode": "function pickLongestString(value) {\n  let best = '';\n  const visit = (v) => {\n    if (typeof v === 'string') {\n      if (v.length > best.length) best = v;\n      return;\n    }\n    if (Array.isArray(v)) {\n      for (const item of v) visit(item);\n      return;\n    }\n    if (v && typeof v === 'object') {\n      for (const key of Object.keys(v)) visit(v[key]);\n    }\n  };\n  visit(value);\n  return best;\n}const content = pickLongestString($input.first().json) || JSON.stringify($input.first().json);\nreturn [{\n  json: {\n    content,\n    fileType: 'google_doc',\n    fileName: $('Normalize Input').first().json.fileName || 'Google Doc',\n    chatId: $('Normalize Input').first().json.chatId,\n    messageId: $('Normalize Input').first().json.messageId,\n    sourceType: 'google_drive_google_doc',\n    runId: $('Normalize Input').first().json.runId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "db8828df-f8f1-4a92-977d-b2d0fa960f45",
      "name": "Extract from PDF",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        1328,
        112
      ],
      "parameters": {
        "options": {},
        "operation": "pdf"
      },
      "typeVersion": 1
    },
    {
      "id": "6753414a-fa67-4168-9bff-9b6379ecdd9e",
      "name": "Normalize PDF",
      "type": "n8n-nodes-base.code",
      "position": [
        1568,
        112
      ],
      "parameters": {
        "jsCode": "function pickLongestString(value) {\n  let best = '';\n  const visit = (v) => {\n    if (typeof v === 'string') {\n      if (v.length > best.length) best = v;\n      return;\n    }\n    if (Array.isArray(v)) {\n      for (const item of v) visit(item);\n      return;\n    }\n    if (v && typeof v === 'object') {\n      for (const key of Object.keys(v)) visit(v[key]);\n    }\n  };\n  visit(value);\n  return best;\n}const content = pickLongestString($input.first().json) || JSON.stringify($input.first().json);\nreturn [{\n  json: {\n    content,\n    fileType: 'pdf',\n    fileName: $('Normalize Input').first().json.fileName,\n    chatId: $('Normalize Input').first().json.chatId,\n    messageId: $('Normalize Input').first().json.messageId,\n    sourceType: 'google_drive_pdf',\n    runId: $('Normalize Input').first().json.runId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "53b6ef38-ad3c-4b50-ac40-e04e5503479a",
      "name": "Prepare Image Data URL",
      "type": "n8n-nodes-base.code",
      "position": [
        1328,
        272
      ],
      "parameters": {
        "jsCode": "const item = $input.first();\nconst binaryKey = $('Normalize Input').first().json.binaryKey || Object.keys(item.binary || {})[0] || 'data';\n\nif (!binaryKey || !item.binary || !item.binary[binaryKey]) {\n  throw new Error('No binary image found on the input item.');\n}\n\nconst bin = item.binary[binaryKey];\nconst mimeType = (bin.mimeType || 'image/jpeg').toLowerCase();\n\nconst buffer = await this.helpers.getBinaryDataBuffer(0, binaryKey);\nconst base64 = Buffer.from(buffer).toString('base64');\n\nif (!base64) {\n  throw new Error('Could not convert input image to base64.');\n}\n\nconst dataUrl = `data:${mimeType};base64,${base64}`;\n\nreturn [{\n  json: {\n    imageDataUrl: dataUrl,\n    fileType: 'image',\n    fileName: $('Normalize Input').first().json.fileName,\n    chatId: $('Normalize Input').first().json.chatId,\n    messageId: $('Normalize Input').first().json.messageId,\n    sourceType: 'google_drive_image',\n    runId: $('Normalize Input').first().json.runId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "79133794-159c-4987-87bb-80c0821922a5",
      "name": "Build Image Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        1568,
        272
      ],
      "parameters": {
        "jsCode": "const payload = {\n  model: 'meta/llama-3.2-11b-vision-instruct',\n  messages: [\n    {\n      role: 'system',\n      content: 'You are an OCR extraction assistant. Extract all readable text from the image exactly as written. Return plain text only. No markdown, no bullets, no commentary, no labels, no JSON.'\n    },\n    {\n      role: 'user',\n      content: [\n        {\n          type: 'text',\n          text: 'Extract all readable text from this image exactly as written. Preserve line breaks when possible. If no readable text is visible, return exactly: NO_TEXT_EXTRACTED'\n        },\n        {\n          type: 'image_url',\n          image_url: {\n            url: $json.imageDataUrl\n          }\n        }\n      ]\n    }\n  ],\n  temperature: 0,\n  top_p: 1,\n  max_tokens: 1800,\n  stream: false\n};\n\nreturn [{\n  json: {\n    nimPayload: JSON.stringify(payload),\n    fileType: $json.fileType,\n    fileName: $json.fileName,\n    chatId: $json.chatId,\n    messageId: $json.messageId,\n    sourceType: $json.sourceType,\n    runId: $json.runId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d5e49e77-6f5a-4421-875d-0e249be73d27",
      "name": "Analyze Image with NVIDIA",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1824,
        272
      ],
      "parameters": {
        "url": "https://integrate.api.nvidia.com/v1/chat/completions",
        "body": "={{ $json.nimPayload }}",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "raw",
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "rawContentType": "application/json",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "5516a7f4-c51c-462d-9a4d-f51bf226e06e",
      "name": "Normalize Image",
      "type": "n8n-nodes-base.code",
      "position": [
        2096,
        272
      ],
      "parameters": {
        "jsCode": "const rawContent = $json.choices?.[0]?.message?.content;\n\nlet text = '';\n\nif (typeof rawContent === 'string') {\n  text = rawContent;\n} else if (Array.isArray(rawContent)) {\n  text = rawContent\n    .map(part => {\n      if (typeof part === 'string') return part;\n      if (part?.type === 'text' && typeof part.text === 'string') return part.text;\n      return '';\n    })\n    .filter(Boolean)\n    .join('\\n');\n}\n\ntext = (text || '')\n  .replace(/```[\\s\\S]*?```/g, '')\n  .trim();\n\nif (!text || text === 'NO_TEXT_EXTRACTED') {\n  text = 'No text extracted';\n}\n\nreturn [{\n  json: {\n    content: text,\n    fileType: $('Build Image Payload').first().json.fileType,\n    fileName: $('Build Image Payload').first().json.fileName,\n    chatId: $('Build Image Payload').first().json.chatId,\n    messageId: $('Build Image Payload').first().json.messageId,\n    sourceType: $('Build Image Payload').first().json.sourceType,\n    runId: $('Build Image Payload').first().json.runId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "75db9680-186f-4594-bda3-a5928b19101b",
      "name": "Extract from Text File",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        1328,
        592
      ],
      "parameters": {
        "options": {},
        "operation": "text"
      },
      "typeVersion": 1
    },
    {
      "id": "42b50c4a-2811-4722-a166-d3d9d807e1c6",
      "name": "Normalize Text File",
      "type": "n8n-nodes-base.code",
      "position": [
        1568,
        592
      ],
      "parameters": {
        "jsCode": "function pickLongestString(value) {\n  let best = '';\n  const visit = (v) => {\n    if (typeof v === 'string') {\n      if (v.length > best.length) best = v;\n      return;\n    }\n    if (Array.isArray(v)) {\n      for (const item of v) visit(item);\n      return;\n    }\n    if (v && typeof v === 'object') {\n      for (const key of Object.keys(v)) visit(v[key]);\n    }\n  };\n  visit(value);\n  return best;\n}const content = pickLongestString($input.first().json) || JSON.stringify($input.first().json);\nreturn [{\n  json: {\n    content,\n    fileType: 'text_file',\n    fileName: $('Normalize Input').first().json.fileName,\n    chatId: $('Normalize Input').first().json.chatId,\n    messageId: $('Normalize Input').first().json.messageId,\n    sourceType: 'google_drive_text_file',\n    runId: $('Normalize Input').first().json.runId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ca006ec2-196b-4611-811c-7c6389526cab",
      "name": "Extract from CSV",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        1328,
        752
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "ad4d4750-c19a-40ab-92d9-c68b48a6d60b",
      "name": "Normalize CSV",
      "type": "n8n-nodes-base.code",
      "position": [
        1568,
        752
      ],
      "parameters": {
        "jsCode": "const rows = $input.all().map(item => item.json);\nconst content = JSON.stringify(rows, null, 2);\nreturn [{\n  json: {\n    content,\n    fileType: 'csv',\n    fileName: $('Normalize Input').first().json.fileName,\n    chatId: $('Normalize Input').first().json.chatId,\n    messageId: $('Normalize Input').first().json.messageId,\n    sourceType: 'google_drive_csv',\n    runId: $('Normalize Input').first().json.runId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f54a8128-9e91-4d2d-b4bf-aed3f7ed5613",
      "name": "Normalize Raw Text",
      "type": "n8n-nodes-base.code",
      "position": [
        832,
        912
      ],
      "parameters": {
        "jsCode": "return [{\n  json: {\n    content: $('Normalize Input').first().json.rawText,\n    fileType: 'raw_text',\n    fileName: 'telegram-message',\n    chatId: $('Normalize Input').first().json.chatId,\n    messageId: $('Normalize Input').first().json.messageId,\n    sourceType: 'google_drive_text',\n    runId: $('Normalize Input').first().json.runId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "105c960a-a06c-43bb-bb85-3944fac89225",
      "name": "Send Unsupported Message",
      "type": "n8n-nodes-base.telegram",
      "position": [
        832,
        1088
      ],
      "parameters": {
        "text": "=Unsupported Google Drive file.\n\nSupported types: PDF, image, Google Docs, text file, and CSV.",
        "chatId": "={{ $('Normalize Input').first().json.chatId }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "c727e5de-c8ba-4d1c-9ee8-1be0794719ae",
      "name": "Build Structuring Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        1904,
        432
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst rawContent = (input.content || '').toString().trim();\nconst trimmedContent = rawContent.slice(0, 12000);\n\nconst schema = {\n  type: \"object\",\n  properties: {\n    title: { type: \"string\" },\n    summary: { type: \"string\" },\n    category: { type: \"string\" },\n    language: { type: \"string\" },\n    key_points: {\n      type: \"array\",\n      items: { type: \"string\" }\n    },\n    confidence_notes: { type: \"string\" }\n  },\n  required: [\"title\", \"summary\", \"category\", \"language\", \"key_points\", \"confidence_notes\"]\n};\n\nconst system = [\n  'You are a document extraction and structuring assistant.',\n  'Structure the already extracted text.',\n  'Return only the schema fields.',\n  'No reasoning.',\n  'No commentary.',\n  'No <think> tags.',\n  'No markdown fences.',\n  'Do not hallucinate missing facts.',\n  'If unknown, use empty string or empty array.'\n].join('\\n');\n\nconst user = [\n  `Source type: ${input.fileType}`,\n  `File name: ${input.fileName}`,\n  '',\n  'Text to structure:',\n  trimmedContent\n].join('\\n');\n\nconst payload = {\n  model: 'nvidia/llama-3.3-nemotron-super-49b-v1.5',\n  messages: [\n    { role: 'system', content: system },\n    { role: 'user', content: user }\n  ],\n  temperature: 0,\n  top_p: 1,\n  max_tokens: 600,\n  stream: false,\n  guided_json: schema\n};\n\nreturn [{\n  json: {\n    nimPayload: JSON.stringify(payload),\n    originalContent: rawContent,\n    fileType: input.fileType,\n    fileName: input.fileName,\n    chatId: input.chatId,\n    messageId: input.messageId,\n    sourceType: input.sourceType,\n    runId: input.runId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9aa50389-31c1-467a-b494-9739001b3b12",
      "name": "Structure Output with NVIDIA",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2208,
        432
      ],
      "parameters": {
        "url": "https://integrate.api.nvidia.com/v1/chat/completions",
        "body": "={{ $json.nimPayload }}",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "raw",
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "rawContentType": "application/json",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "9ae98693-24ea-4bee-8cfc-7f47a174f299",
      "name": "Parse Structured Output",
      "type": "n8n-nodes-base.code",
      "position": [
        2464,
        432
      ],
      "parameters": {
        "jsCode": "function safeParse(text) {\n  if (!text) return null;\n  try {\n    return JSON.parse(text);\n  } catch (e) {}\n  const match = text.match(/\\{[\\s\\S]*\\}/);\n  if (match) {\n    try {\n      return JSON.parse(match[0]);\n    } catch (e) {}\n  }\n  return null;\n}\n\nconst raw = $json.choices?.[0]?.message?.content?.trim() || '';\nconst cleanedRaw = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\nconst parsed = safeParse(cleanedRaw) || {};\nconst original = $('Build Structuring Payload').first().json.originalContent || '';\n\nreturn [{\n  json: {\n    title: parsed.title || $('Build Structuring Payload').first().json.fileName || 'Untitled',\n    summary: parsed.summary || original.slice(0, 300),\n    category: parsed.category || '',\n    language: parsed.language || '',\n    key_points: Array.isArray(parsed.key_points) ? parsed.key_points : [],\n    confidence_notes: parsed.confidence_notes || '',\n    extracted_text: original,\n    sourceType: $('Build Structuring Payload').first().json.sourceType,\n    fileType: $('Build Structuring Payload').first().json.fileType,\n    fileName: $('Build Structuring Payload').first().json.fileName,\n    chatId: $('Build Structuring Payload').first().json.chatId,\n    messageId: $('Build Structuring Payload').first().json.messageId,\n    runId: $('Build Structuring Payload').first().json.runId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c9c31b49-b926-46a8-a258-513b99098928",
      "name": "Append Row in Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2768,
        336
      ],
      "parameters": {
        "columns": {
          "value": {
            "Title": "={{ $json.title }}",
            "Run ID": "={{ $json.runId }}",
            "Summary": "={{ $json.summary }}",
            "Category": "={{ $json.category }}",
            "Language": "={{ $json.language }}",
            "File Name": "={{ $json.fileName }}",
            "File Type": "={{ $json.fileType }}",
            "Timestamp": "={{ new Date().toISOString() }}",
            "Key Points": "={{ ($json.key_points || []).join(' | ') }}",
            "Source Type": "={{ $json.sourceType }}",
            "Extracted Text": "={{ $json.extracted_text }}",
            "Confidence Notes": "={{ $json.confidence_notes }}"
          },
          "schema": [
            {
              "id": "Timestamp",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Source Type",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Source Type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "File Type",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "File Type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "File Name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "File Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Title",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Summary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Summary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Category",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Language",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Language",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Key Points",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Key Points",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Confidence Notes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Confidence Notes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Extracted Text",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Extracted Text",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Run ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Run ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Extract_Log"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_GOOGLE_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5,
      "continueOnFail": true
    },
    {
      "id": "20cbfebd-8602-40f4-8b79-307a7f9d9dbd",
      "name": "Build Telegram Reply",
      "type": "n8n-nodes-base.code",
      "position": [
        2768,
        528
      ],
      "parameters": {
        "jsCode": "let text = [\n  'FILE PROCESSED',\n  '',\n  `Name: ${$json.fileName || ''}`,\n  `Type: ${$json.fileType || ''}`,\n  `Title: ${$json.title || ''}`,\n  `Category: ${$json.category || ''}`,\n  '',\n  'Status: Extracted and logged to Google Sheets.'\n].filter(Boolean).join('\\n');\n\nif (text.length > 3500) {\n  text = text.slice(0, 3490) + '\\n\u2026';\n}\n\nreturn [{\n  json: {\n    replyText: text,\n    chatId: $json.chatId,\n    messageId: ''\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "e447d8ab-4e99-4e47-8add-5a6ae9003a55",
      "name": "Send Reply",
      "type": "n8n-nodes-base.telegram",
      "position": [
        2992,
        528
      ],
      "parameters": {
        "text": "={{ $json.replyText }}",
        "chatId": "={{ $json.chatId }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "a714101a-c2c2-4ab8-a231-f7d7231217c5",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        64,
        640
      ],
      "parameters": {
        "color": 7,
        "width": 608,
        "height": 336,
        "content": "## Step 2 \u2014 Credentials\n\nCreate and connect these credentials before testing:\n\n- Google Drive OAuth2 account \u2014 used by `Google Drive Trigger` and `Download Drive File`\n- Google Docs OAuth2 account \u2014 used by `Get Google Doc`\n- Google Sheets OAuth2 account \u2014 used by `Append Row in Sheet`\n- Telegram credential \u2014 used by `Send Reply` and `Send Unsupported Message`\n- HTTP Header Auth credential for NVIDIA NIM \u2014 set `Authorization: Bearer YOUR_API_KEY` and apply it to both NVIDIA HTTP Request nodes\n\nAfter connecting credentials, re-open each node once and confirm the correct account is selected."
      },
      "typeVersion": 1
    },
    {
      "id": "44bc95df-dcdc-404a-8ac6-08172e47e46e",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2720,
        800
      ],
      "parameters": {
        "color": 7,
        "width": 512,
        "height": 448,
        "content": "## Step 4 \u2014 Activate\n\nBefore activating the workflow, test one file at a time in this order:\n\n- PDF\n- text file\n- CSV\n- image\n- Google Doc\n\nFor each test, confirm:\n- the correct branch ran\n- extracted text was produced\n- structured output was returned\n- a row was appended to `Extract_Log`\n- the Telegram reply was sent successfully\n\nActivate the workflow only after all supported file paths pass at least one full end-to-end test."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Route Input": {
      "main": [
        [
          {
            "node": "Download Drive File",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Download Drive File",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Google Doc",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Download Drive File",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Download Drive File",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Unsupported Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize CSV": {
      "main": [
        [
          {
            "node": "Build Structuring Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize PDF": {
      "main": [
        [
          {
            "node": "Build Structuring Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Google Doc": {
      "main": [
        [
          {
            "node": "Normalize Google Doc",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Image": {
      "main": [
        [
          {
            "node": "Build Structuring Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Input": {
      "main": [
        [
          {
            "node": "Route Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from CSV": {
      "main": [
        [
          {
            "node": "Normalize CSV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from PDF": {
      "main": [
        [
          {
            "node": "Normalize PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Raw Text": {
      "main": [
        [
          {
            "node": "Build Structuring Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Image Payload": {
      "main": [
        [
          {
            "node": "Analyze Image with NVIDIA",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Drive File": {
      "main": [
        [
          {
            "node": "Route Downloaded File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Text File": {
      "main": [
        [
          {
            "node": "Build Structuring Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Telegram Reply": {
      "main": [
        [
          {
            "node": "Send Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Drive Trigger": {
      "main": [
        [
          {
            "node": "Normalize Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Google Doc": {
      "main": [
        [
          {
            "node": "Build Structuring Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Downloaded File": {
      "main": [
        [
          {
            "node": "Extract from PDF",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Image Data URL",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract from Text File",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract from CSV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from Text File": {
      "main": [
        [
          {
            "node": "Normalize Text File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Image Data URL": {
      "main": [
        [
          {
            "node": "Build Image Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Structured Output": {
      "main": [
        [
          {
            "node": "Append Row in Sheet",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build Telegram Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Image with NVIDIA": {
      "main": [
        [
          {
            "node": "Normalize Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Structuring Payload": {
      "main": [
        [
          {
            "node": "Structure Output with NVIDIA",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Structure Output with NVIDIA": {
      "main": [
        [
          {
            "node": "Parse Structured Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}