AutomationFlowsData & Sheets › Smart Folder2table

Smart Folder2table

Smart-Folder2Table. Uses executeWorkflowTrigger, httpRequest, chainLlm, lmChatGroq. Event-driven trigger; 26 nodes.

Event trigger★★★★☆ complexityAI-powered26 nodesExecute Workflow TriggerHTTP RequestChain LlmGroq ChatOutput Parser StructuredGoogle DriveGoogle Sheets
Data & Sheets Trigger: Event Nodes: 26 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow follows the Chainllm → Execute Workflow Trigger recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -1472,
        -928
      ],
      "id": "4c5644fb-65b1-45e4-a048-d0a30f503b37",
      "name": "Manual Trigger"
    },
    {
      "parameters": {
        "inputSource": "passthrough"
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        -1456,
        -1152
      ],
      "id": "bed55fa5-8542-4f12-b6c8-30a1c51e7d7a",
      "name": "When Executed by Another Workflow"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "fp-folder-id",
              "name": "folder_id",
              "value": "={{ $json.folder_id || '' }}",
              "type": "string"
            },
            {
              "id": "fp-spreadsheet-id",
              "name": "spreadsheet_id",
              "value": "={{ $json.spreadsheet_id || '' }}",
              "type": "string"
            },
            {
              "id": "fp-data-sheet-name",
              "name": "data_sheet_name",
              "value": "={{ $json.data_sheet_name || 'Sheet1' }}",
              "type": "string"
            },
            {
              "id": "fp-rate-limit-wait",
              "name": "rate_limit_wait_seconds",
              "value": "={{ $json.rate_limit_wait_seconds || 0 }}",
              "type": "number"
            },
            {
              "id": "fp-source-file-col",
              "name": "source_file_column",
              "value": "={{ $json.source_file_column || 'source_file' }}",
              "type": "string"
            },
            {
              "id": "fp-file-include",
              "name": "file_include",
              "value": "={{ $json.file_include || 'all' }}",
              "type": "string"
            },
            {
              "id": "fp-file-exclude",
              "name": "file_exclude",
              "value": "={{ $json.file_exclude || '_.png, __.png' }}",
              "type": "string"
            },
            {
              "id": "fp-file-limit",
              "name": "file_limit",
              "value": "={{ $json.file_limit || null }}",
              "type": "number"
            },
            {
              "id": "fp-schema-sheet-name",
              "name": "schema_sheet_name",
              "value": "={{ $json.schema_sheet_name || 'Description_hig7f6' }}",
              "type": "string"
            },
            {
              "id": "fp-match-same-row",
              "name": "match_same_row",
              "value": "={{ $json.match_same_row === true }}",
              "type": "boolean"
            },
            {
              "id": "fp-match-column",
              "name": "match_column",
              "value": "={{ $json.match_column || 'source_file' }}",
              "type": "string"
            },
            {
              "id": "fp-batch-size",
              "name": "batch_size",
              "value": "={{ $json.batch_size || 7 }}",
              "type": "number"
            },
            {
              "id": "fp-max-file-size-mb",
              "name": "max_file_size_mb",
              "value": "={{ $json.max_file_size_mb || null }}",
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -1232,
        -1152
      ],
      "id": "54a9d8b7-b56b-4de6-9df2-ab48b430debc",
      "name": "Config"
    },
    {
      "parameters": {
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $json.spreadsheet_id }}?fields=sheets(properties.title,data.rowData.values.userEnteredValue)&includeGridData=true",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -1008,
        -1152
      ],
      "id": "a1f9ce38-aa27-4425-afc8-f07bab9d9b26",
      "name": "Fetch Data & Schema Sheets"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "schema-exists-condition",
              "leftValue": "={{ $json.sheets?.some(s => s.properties?.title === $('Config').first().json.schema_sheet_name && s.data?.[0]?.rowData?.length > 1) }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "9f8dfaa5-80d0-434e-982f-1c37ddcbf55a",
      "name": "IF: Schema Exists?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -784,
        -1152
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=You are a database schema expert. Given these column names from a spreadsheet:\n{{ (() => { const config = $('Config').first().json; const sheets = $('Fetch Data & Schema Sheets').first().json.sheets || []; const dataSheet = sheets.find(s => s.properties?.title === config.data_sheet_name); const row0 = dataSheet?.data?.[0]?.rowData?.[0]; return row0 ? (row0.values || []).map(cell => { const uev = cell?.userEnteredValue; if (!uev) return ''; return uev.stringValue ?? uev.numberValue ?? uev.boolValue ?? ''; }) : []; })() }}\n\nGenerate a schema definition for each column. Output a JSON array where each item has:\n- ColumnName: the exact column name (preserve case)\n- Type: one of [str, int, date, list, class]\n- Description: brief description (10 words max) of what this field stores\n- Classes: if Type is \"class\", provide 2-4 comma-separated enum values; otherwise empty string\n\nType guidelines:\n- date: timestamps, birthdays, created_at, updated_at, any date field\n- int: IDs, counts, frequencies, ages, numeric identifiers\n- list: multiple values (emails, phones, jobs, tags, anything plural)\n- class: categorical/status fields (use Classes for enum values)\n- str: everything else (names, addresses, notes, single text values)\n\nIMPORTANT: Output ONLY the JSON array, no explanation.",
        "hasOutputParser": true,
        "batching": {}
      },
      "id": "048abaa1-73cc-4c24-83bc-8cb08f922369",
      "name": "LLM: Generate Schema",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        -1056,
        -912
      ]
    },
    {
      "parameters": {
        "model": "openai/gpt-oss-120b",
        "options": {}
      },
      "id": "e652c6b0-f480-429a-b9bd-0b668e730935",
      "name": "Schema LLM",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "typeVersion": 1,
      "position": [
        -1056,
        -736
      ]
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"array\",\n  \"items\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"ColumnName\": { \"type\": \"string\" },\n      \"Type\": { \"type\": \"string\", \"enum\": [\"str\", \"int\", \"date\", \"list\", \"class\"] },\n      \"Description\": { \"type\": \"string\" },\n      \"Classes\": { \"type\": \"string\" }\n    },\n    \"required\": [\"ColumnName\", \"Type\", \"Description\", \"Classes\"]\n  }\n}"
      },
      "id": "50c7d475-572e-44da-a352-db52e2b07616",
      "name": "Schema Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        -912,
        -736
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $('Config').first().json.spreadsheet_id }}:batchUpdate",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ (() => { const id = Date.now() % 1000000; const name = $('Config').first().json.schema_sheet_name; const rows = $input.first().json.output; return JSON.stringify({ requests: [{ addSheet: { properties: { sheetId: id, title: name } } }, { updateCells: { rows: [{ values: [\"ColumnName\",\"Type\",\"Description\",\"Classes\"].map(h => ({ userEnteredValue: { stringValue: h } })) }].concat(rows.map(r => ({ values: [\"ColumnName\",\"Type\",\"Description\",\"Classes\"].map(c => ({ userEnteredValue: { stringValue: String(r[c]||\"\") } })) }))), fields: \"userEnteredValue\", start: { sheetId: id, rowIndex: 0, columnIndex: 0 } } }] }); })() }}",
        "options": {}
      },
      "id": "b88ebb72-d331-44a7-87e5-a98d3dcf3e42",
      "name": "Create & Write Schema Sheet",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -768,
        -912
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $('Config').first().json.spreadsheet_id }}/values:batchUpdate",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ (() => { const config = $('Config').first().json; const sheets = $('Fetch Data & Schema Sheets').first().json.sheets || []; const dataSheet = sheets.find(s => s.properties?.title === config.data_sheet_name); const row0 = dataSheet?.data?.[0]?.rowData?.[0]; const headers = row0 ? (row0.values || []).map(cell => { const uev = cell?.userEnteredValue; if (!uev) return ''; return uev.stringValue ?? uev.numberValue ?? uev.boolValue ?? ''; }) : []; const colLetter = (n) => { let s = ''; n++; while (n > 0) { n--; s = String.fromCharCode(65 + (n % 26)) + s; n = Math.floor(n / 26); } return s; }; const sheetName = config.data_sheet_name; const required = ['source_file', 'Text_to_interpret']; const missing = required.filter(h => !headers.includes(h)); const data = missing.map((h, i) => ({ range: sheetName + '!' + colLetter(headers.length + i) + '1', values: [[h]] })); return JSON.stringify({ valueInputOption: 'RAW', data }); })() }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -528,
        -1152
      ],
      "id": "085413c5-74c1-4604-9f5d-3753431e080e",
      "name": "Ensure Headers",
      "notes": "folder2table-only; no equivalent in smart-table-fill"
    },
    {
      "parameters": {
        "resource": "fileFolder",
        "returnAll": true,
        "filter": {
          "folderId": {
            "__rl": true,
            "value": "={{ $('Config').first().json.folder_id || 'ERROR_config_EMPTY_FOLDER_ID' }}",
            "mode": "id"
          },
          "whatToSearch": "files"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        -304,
        -1152
      ],
      "id": "fp-list-drive-files",
      "name": "List Drive Files"
    },
    {
      "parameters": {
        "jsCode": "const config = $('Config').first().json;\nconst batchResponse = $('Fetch Data & Schema Sheets').first().json;\nconst sheets = batchResponse.sheets || [];\n\nconst extractValues = (rowData) => {\n  if (!rowData) return [];\n  return rowData.map(row =>\n    (row.values || []).map(cell => {\n      const uev = cell?.userEnteredValue;\n      if (!uev) return '';\n      return uev.stringValue ?? uev.numberValue ?? uev.boolValue ?? '';\n    })\n  );\n};\n\nconst schemaSheet = sheets.find(s => s.properties?.title === config.schema_sheet_name);\nconst schemaValues = schemaSheet ? extractValues(schemaSheet.data?.[0]?.rowData) : [];\nconst dataSheet = sheets.find(s => s.properties?.title === config.data_sheet_name);\nconst dataValues = dataSheet ? extractValues(dataSheet.data?.[0]?.rowData) : [];\n\n// Try to get schema from multiple sources\nlet schemaRows = [];\n\n// Source 1: If schema was just created via LLM, use that output\ntry {\n  const llmOutput = $('LLM: Generate Schema').first().json.output;\n  if (Array.isArray(llmOutput) && llmOutput.length > 0) {\n    schemaRows = llmOutput;\n  }\n} catch (e) {\n  // LLM path not taken, try sheet data\n}\n\n// Source 2: If schema exists in sheet, parse from raw response\nif (schemaRows.length === 0) {\n  const schemaHeaders = schemaValues[0] || [];\n  schemaRows = schemaValues.slice(1).map(row => {\n    const obj = {};\n    schemaHeaders.forEach((h, i) => obj[h] = row[i] || '');\n    return obj;\n  });\n}\n\n// Get data sheet info for filtering already-processed files\nconst dataHeaders = dataValues[0] || [];\nconst dataRows = dataValues.slice(1).map(row => {\n  const obj = {};\n  dataHeaders.forEach((h, i) => obj[h] = row[i] || '');\n  return obj;\n});\n\nconst driveFiles = $input.all();\n\n// Build extraction object from schema\nconst internalFields = ['source_file', 'text_to_interpret', 'row_number'];\nconst userColumns = schemaRows.filter(row => {\n  const name = (row.ColumnName || '').trim();\n  return name && !internalFields.includes(name.toLowerCase());\n});\n\n// Validate: class columns must have Classes defined\nfor (const row of userColumns) {\n  if (row.Type === 'class') {\n    const vals = (row.Classes || '').split(',').map(s => s.trim()).filter(Boolean);\n    if (vals.length === 0) {\n      throw new Error(\n        `Schema error in schema sheet (${config.schema_sheet_name}): column \"${row.ColumnName}\" has Type \"class\" but the \"Classes\" column is empty. ` +\n        `Fix: add comma-separated values to the \"Classes\" column for \"${row.ColumnName}\".`\n      );\n    }\n  }\n}\n\nlet extraction = {};\nif (userColumns.length > 0) {\n  const focus_fields = userColumns.map(r => r.ColumnName);\n  const field_schemas = userColumns.map(r => ({\n    name: r.ColumnName,\n    type: r.Type || 'str',\n    description: r.Description || '',\n    classes: r.Classes || ''\n  }));\n  \n  const instructionLines = userColumns.map(r => {\n    const name = r.ColumnName;\n    const type = r.Type || 'str';\n    const desc = r.Description || '';\n    const classes = r.Classes || '';\n    let line = `- ${name}`;\n    if (type === 'class' && classes) line += ` (enum: ${classes})`;\n    else if (type === 'list') line += ` (array)`;\n    else if (type === 'date') line += ` (date)`;\n    else if (type === 'int') line += ` (number)`;\n    if (desc) line += `: ${desc}`;\n    return line;\n  });\n  \n  extraction = {\n    type: 'document_analysis',\n    focus_fields,\n    field_schemas,\n    instructions: `Extract ALL visible information from the document. Additionally, PRIORITIZE these fields (use these exact key names if the information is present):\\n${instructionLines.join('\\n')}\\n\\nInclude any other relevant information you observe (logos, text, context, etc.).`\n  };\n}\n\n// Filter out already-processed files (resumability)\nconst col = config.source_file_column;\nconst done = new Set();\nfor (const r of dataRows) {\n  const val = (r[col] || '').trim();\n  if (val) done.add(val.toLowerCase());\n}\n\nlet filtered = driveFiles.filter(f => {\n  const name = (f.json.name || '').trim().toLowerCase();\n  return name && !done.has(name);\n});\n\n// Include filter: if not \"all\", only keep files in the list\nconst includeRaw = (config.file_include || 'all').trim();\nif (includeRaw.toLowerCase() !== 'all') {\n  const includeSet = new Set(\n    includeRaw.split(',').map(s => s.trim().toLowerCase()).filter(Boolean)\n  );\n  filtered = filtered.filter(f =>\n    includeSet.has((f.json.name || '').trim().toLowerCase())\n  );\n}\n\n// Exclude filter: remove files in the list\nconst excludeRaw = (config.file_exclude || '').trim();\nif (excludeRaw) {\n  const excludeSet = new Set(\n    excludeRaw.split(',').map(s => s.trim().toLowerCase()).filter(Boolean)\n  );\n  filtered = filtered.filter(f =>\n    !excludeSet.has((f.json.name || '').trim().toLowerCase())\n  );\n}\n\n// Limit: cap the number of files to process\nconst fileLimit = config.file_limit;\nif (fileLimit != null && fileLimit > 0) {\n  filtered = filtered.slice(0, fileLimit);\n}\n\n// Size filter: skip files exceeding max_file_size_mb\nconst maxSizeMB = config.max_file_size_mb;\nif (maxSizeMB != null && maxSizeMB > 0) {\n  const maxBytes = maxSizeMB * 1024 * 1024;\n  filtered = filtered.filter(f => {\n    const sizeBytes = parseInt(f.json.size || '0', 10);\n    return sizeBytes <= maxBytes;\n  });\n}\n\nreturn filtered.map(f => ({ json: { ...f.json, _extraction: extraction } }));"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -80,
        -1152
      ],
      "id": "fp-prepare-and-filter",
      "name": "Build Output Schema & Filter"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        128,
        -1152
      ],
      "id": "fp-loop",
      "name": "Loop Over Files"
    },
    {
      "parameters": {
        "operation": "download",
        "fileId": {
          "__rl": true,
          "value": "={{ $json.id }}",
          "mode": "id"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        352,
        -1136
      ],
      "id": "fp-download-file",
      "name": "Download File"
    },
    {
      "parameters": {
        "jsCode": "const item = $input.first();\nconst config = $('Config').first().json;\nconst batchSize = config.batch_size || 10;\nconst extraction = $('Build Output Schema & Filter').first().json._extraction || {};\nconst fieldSchemas = extraction.field_schemas || [];\n\n// No batching needed \u2014 pass through unchanged\nif (fieldSchemas.length <= batchSize) {\n  return [{ json: { ...item.json, _extraction: extraction }, binary: item.binary }];\n}\n\n// Split field_schemas into batches, rebuild extraction per batch\nconst batches = [];\nfor (let i = 0; i < fieldSchemas.length; i += batchSize) {\n  const batchSchemas = fieldSchemas.slice(i, i + batchSize);\n  const instructionLines = batchSchemas.map(r => {\n    const name = r.name;\n    const type = r.type || 'str';\n    const desc = r.description || '';\n    const classes = r.classes || '';\n    let line = `- ${name}`;\n    if (type === 'class' && classes) line += ` (enum: ${classes})`;\n    else if (type === 'list') line += ` (array)`;\n    else if (type === 'date') line += ` (date)`;\n    else if (type === 'int') line += ` (number)`;\n    if (desc) line += `: ${desc}`;\n    return line;\n  });\n  batches.push({\n    type: extraction.type || 'document_analysis',\n    focus_fields: batchSchemas.map(s => s.name),\n    field_schemas: batchSchemas,\n    instructions: `Extract ALL visible information from the document. Additionally, PRIORITIZE these fields (use these exact key names if the information is present):\\n${instructionLines.join('\\n')}\\n\\nInclude any other relevant information you observe (logos, text, context, etc.).`\n  });\n}\n\nreturn batches.map(batch => ({\n  json: { ...item.json, _extraction: batch },\n  binary: item.binary\n}));"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -528,
        -848
      ],
      "id": "fp-convert-file",
      "name": "Expand Batches"
    },
    {
      "parameters": {
        "amount": "={{ $('Loop Over Files').context['currentRunIndex'] === 0 ? 0 : ($('Config').first().json.rate_limit_wait_seconds || 0) }}"
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        -80,
        -848
      ],
      "id": "fp-rate-limit-wait",
      "name": "Rate Limit Wait"
    },
    {
      "parameters": {
        "jsCode": "const fileName = $('Download File').first().json.name || 'unknown';\nconst allOutputs = $input.all();\n\n// Merge all batch outputs into one object\nconst merged = {};\nlet lastRawText = '';\nfor (const output of allOutputs) {\n  const converterOutput = output.json;\n  const rawText = converterOutput?.data?.text || converterOutput?.text || '';\n  if (!rawText.trim()) continue;\n  lastRawText = rawText;\n\n  try {\n    let parsed = JSON.parse(rawText);\n    if (Array.isArray(parsed)) parsed = parsed[0] || {};\n    if (typeof parsed === 'object' && parsed !== null) {\n      Object.assign(merged, parsed);\n    }\n  } catch (e) {\n    if (allOutputs.length === 1) {\n      Object.assign(merged, { Text_to_interpret: rawText });\n    }\n  }\n}\n\nif (Object.keys(merged).length === 0 && !lastRawText.trim()) {\n  console.warn(`Skipped file \"${fileName}\": converter returned empty output`);\n  return [];\n}\n\nconst rowData = {\n  ...merged,\n  source_file: fileName,\n  Text_to_interpret: lastRawText,\n};\n\ndelete rowData.content_class;\ndelete rowData.class_confidence;\ndelete rowData.confidence;\n\nreturn [{ json: rowData }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        144,
        -848
      ],
      "id": "fp-prepare-write-data",
      "name": "Prepare Write Data"
    },
    {
      "parameters": {
        "operation": "={{ $('Config').first().json.match_same_row ? 'appendOrUpdate' : 'append' }}",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Config').first().json.spreadsheet_id }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Config').first().json.data_sheet_name }}",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {
            "source_file": "={{ $json.source_file }}"
          },
          "matchingColumns": [
            "source_file"
          ],
          "schema": [
            {
              "id": "source_file",
              "displayName": "source_file",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "handlingExtraData": "ignoreIt"
        },
        "startIndex": "A"
      },
      "id": "75803755-04df-46a7-918f-e65a0854ef37",
      "name": "Write Extracted Row",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        368,
        -848
      ]
    },
    {
      "parameters": {
        "content": "## Intelligent Document Extraction to Google Sheets\nProcesses all files in a Google Drive folder through any-file2json-converter and writes directly to sheet.\n\n### How it works\n1. Triggers manually or via another workflow to process documents stored in a specific Google Drive folder.\n2. Analyzes your Google Sheet to find or create an AI-powered extraction schema based on your existing column headers.\n3. Lists folder contents and filters out files that were already processed to prevent duplicates and enable resumability.\n4. Iterates through each file, downloading the content and passing it to a sub-workflow for AI-driven data extraction.\n5. Cleans and structures the extracted information before appending or updating rows in your target Google Sheet.\n\n### Setup\n- [ ] Connect **Google Drive** and **Google Sheets** credentials.\n- [ ] Create a Sheet with column headers for the data to extract (e.g., Date, Amount, Vendor).\n- [ ] Paste the **Folder ID** and **Spreadsheet ID** into the **Config** node.\n- [ ] Add an **OpenAI** or **Groq** API key in the **Schema LLM** node.\n- [ ] Ensure `any-file2json-converter` sub-workflow is active.\n- [ ] (Optional) Adjust `batch_size` / `rate_limit_wait_seconds` in **Config** to match your API limits.\n\nAlso supports: auto-schema creation, resumability, file filtering, column batching, adaptive rate limiting.\n[Setup guide](https://github.com/runfish5/micro-services/blob/main/projects/n8n/02_smart-table-fill/docs/setup-guide-folder2table.md)",
        "height": 700,
        "width": 472
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1968,
        -1152
      ],
      "id": "e54faa52-75e6-42ea-8958-334f5b54b833",
      "name": "Sticky Note - Setup"
    },
    {
      "parameters": {
        "content": "### Schema Check & Creation\nOn first run, if schema sheet doesn't exist:\n1. Reads column headers from data sheet\n2. Uses LLM to generate Type, Description, Classes\n3. Creates schema sheet with intelligent defaults\n\nEdit the schema sheet to customize extraction behavior.",
        "height": 272,
        "width": 444,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1072,
        -944
      ],
      "id": "79281892-ef49-4f17-962f-ebc251e1b38f",
      "name": "Sticky Note - Schema"
    },
    {
      "parameters": {
        "content": "### Loop: 1 file at a time\n\n\n\n\n\n\n\n\n\n\nDownload \u2192 Convert \u2192 Write \u2192 next file.\nIf file #6 fails, files 1-5 are already written.\nOn retry, resumability check skips those 5.",
        "height": 232,
        "width": 308,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        32,
        -1184
      ],
      "id": "fp-sticky-mode",
      "name": "Sticky Note - Loop"
    },
    {
      "parameters": {
        "content": "### If stops here\n\nExecute Workflow inputs get cleared on re-import. Re-add the **extraction** input:\n```\n{{ $json._extraction || $('Build Output Schema & Filter').first().json._extraction || {} }}\n```",
        "height": 80,
        "width": 150
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -332,
        -880
      ],
      "id": "fp-sticky-inputs",
      "name": "Sticky Note - Inputs"
    },
    {
      "parameters": {
        "content": "### Dynamic Rate Limit\n\n**Start fast, adapt on error:**\n1. Manual run: rate_limit_wait_seconds = 0 (no delay)\n2. On 429 error: error handler extracts \"retry in Xs\"\n3. Error handler calls folder-processor via Execute Workflow\n   with rate_limit_wait_seconds = extracted timing\n4. Resumability: Already-processed files are skipped\n\n**Config reads from workflow input with fallbacks:**\nManual Trigger \u2192 Config uses defaults (0s wait)\nExecute Workflow \u2192 Config uses passed values",
        "height": 80,
        "width": 150
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -112,
        -880
      ],
      "id": "fp-sticky-rate-limit",
      "name": "Sticky Note - Rate Limit"
    },
    {
      "parameters": {
        "workflowId": {
          "__rl": true,
          "value": "GtcLjBMusAUB0h30",
          "mode": "list",
          "cachedResultName": "any-file2json-converter"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "extraction": "={{ $json._extraction || $('Build Output Schema & Filter').first().json._extraction || {} }}"
          },
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "waitForSubWorkflow": true
        }
      },
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.2,
      "position": [
        -304,
        -848
      ],
      "id": "c73c8a27-0976-4471-a25f-a23add508009",
      "name": "Convert File to Text",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "content": "\ud83d\udd17 [any-file2json-converter source](https://github.com/runfish5/micro-services/blob/main/projects/n8n/03_any-file2json-converter/workflows/any-file2json-converter.json)",
        "height": 60,
        "width": 220
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -324,
        -876
      ],
      "id": "b2c3d4e5-source-link-convert-file",
      "name": "Sticky Note - Subworkflow Source"
    },
    {
      "parameters": {
        "content": "### If stops here\n\nA column has `Type: class` but the `Classes` field is empty.\n\n**Fix:** Open the schema sheet and add comma-separated class values for that column.\n\nOr change the `Type` from `class` to `str`.",
        "height": 100,
        "width": 150
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -104,
        -1184
      ],
      "id": "b2c3d4e5-empty-class-guard-f2t",
      "name": "Sticky Note - Empty Class Guard"
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Fetch Data & Schema Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Data & Schema Sheets": {
      "main": [
        [
          {
            "node": "IF: Schema Exists?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Schema Exists?": {
      "main": [
        [
          {
            "node": "Ensure Headers",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "LLM: Generate Schema",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM: Generate Schema": {
      "main": [
        [
          {
            "node": "Create & Write Schema Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schema LLM": {
      "ai_languageModel": [
        [
          {
            "node": "LLM: Generate Schema",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Schema Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "LLM: Generate Schema",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Create & Write Schema Sheet": {
      "main": [
        [
          {
            "node": "Ensure Headers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ensure Headers": {
      "main": [
        [
          {
            "node": "List Drive Files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List Drive Files": {
      "main": [
        [
          {
            "node": "Build Output Schema & Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Output Schema & Filter": {
      "main": [
        [
          {
            "node": "Loop Over Files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Files": {
      "main": [
        [],
        [
          {
            "node": "Download File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download File": {
      "main": [
        [
          {
            "node": "Expand Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Expand Batches": {
      "main": [
        [
          {
            "node": "Convert File to Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rate Limit Wait": {
      "main": [
        [
          {
            "node": "Prepare Write Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Write Data": {
      "main": [
        [
          {
            "node": "Write Extracted Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Extracted Row": {
      "main": [
        [
          {
            "node": "Loop Over Files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert File to Text": {
      "main": [
        [
          {
            "node": "Rate Limit Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "meta": {
    "templateCredsSetupCompleted": true
  }
}
Pro

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

About this workflow

Smart-Folder2Table. Uses executeWorkflowTrigger, httpRequest, chainLlm, lmChatGroq. Event-driven trigger; 26 nodes.

Source: https://github.com/runfish5/micro-services/blob/main/projects/n8n/02_smart-table-fill/workflows/smart-folder2table.json — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Data & Sheets

Smart-Table-Fill.N8N. Uses httpRequest, googleSheets, chainLlm, outputParserStructured. Event-driven trigger; 31 nodes.

HTTP Request, Google Sheets, Chain Llm +3
Data & Sheets

Clean Optimized Workflow. Uses googleSheets, chainLlm, lmChatGroq, outputParserStructured. Event-driven trigger; 24 nodes.

Google Sheets, Chain Llm, Groq Chat +2
Data & Sheets

This workflow demonstrates a simple way to run evals on a set of test cases stored in a Google Sheet.

Google Sheets, Output Parser Structured, Chain Llm +3
Data & Sheets

This workflow demonstrates a simple way to run evals on a set of test cases stored in a Google Sheet.

OpenRouter Chat, Google Drive, Google Sheets +2
Data & Sheets

simon-apify-youtube-transcript-20250724-v1-ok. Uses httpRequest, lmChatOpenAi, chainLlm, executeWorkflowTrigger. Event-driven trigger; 19 nodes.

HTTP Request, OpenAI Chat, Chain Llm +3