AutomationFlowsData & Sheets › Smart-table-fill.n8n

Smart-table-fill.n8n

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

Event trigger★★★★★ complexityAI-powered31 nodesHTTP RequestGoogle SheetsChain LlmOutput Parser StructuredGroq ChatExecute Workflow Trigger
Data & Sheets Trigger: Event Nodes: 31 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": {
        "content": "## Smart Table Fill \u2013 Extract Structured Data\n\n### How it works\n1. Workflow is triggered by another workflow.\n2. It reads rows from the configured data sheet and loads column headers.\n3. If a schema sheet exists it uses it; otherwise it asks an LLM to generate a JSON schema for the columns and creates the schema sheet.\n4. The rows are batched and sent to an LLM that extracts the required fields from the text column according to the schema.\n5. LLM output is parsed, confidence scores are added, duplicate emails are merged, and each row's data is assembled.\n6. The final structured records are written back to the data sheet (append or update).\n\n### Setup\n- [ ] Connect your Google Sheets account in n8n credentials.\n- [ ] Set `spreadsheet_id` to the target spreadsheet.\n- [ ] Ensure the data sheet (default \"Sheet1\") contains a header row with a text column to interpret.\n- [ ] (Optional) Provide a custom `schema_sheet_name` or let the workflow generate one.\n- [ ] Add your Groq/OpenAI API key for the LLM nodes.\n- [ ] Adjust `batch_size` if you hit LLM context limits.\n- [ ] Call this workflow from another n8n workflow or test with the manual trigger.",
        "height": 500,
        "width": 500,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1150,
        -380
      ],
      "id": "f7a8b9c0-d1e2-4f3a-5b6c-7d8e9f0a1b2c",
      "name": "Sticky Note - Template Description"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "emails-folder-id",
              "name": "emails_folder_id",
              "value": "={{ $json.response.result.emails_folder_id }}",
              "type": "string"
            },
            {
              "id": "folder-id",
              "name": "folder_id",
              "value": "={{ $json.response.result.folder_id }}",
              "type": "string"
            },
            {
              "id": "email-subject",
              "name": "email_subject",
              "value": "={{ $('String Input').first().json.subject }}",
              "type": "string"
            },
            {
              "id": "body-core",
              "name": "body_core",
              "value": "={{ $('String Input').first().json.body_core }}",
              "type": "string"
            },
            {
              "id": "sender-name",
              "name": "contact_name",
              "value": "={{ $('String Input').first().json.contact_name }}",
              "type": "string"
            },
            {
              "id": "sender-email",
              "name": "contact_email",
              "value": "={{ $('String Input').first().json.contact_email }}",
              "type": "string"
            },
            {
              "id": "48ac7e66-b0b4-4012-8798-0747ebbfbaa4",
              "name": "direction",
              "value": "={{ $('When Executed by Another Workflow').first().json.direction }}",
              "type": "string"
            },
            {
              "id": "2251d600-66ac-4840-8d98-b4e48600b14a",
              "name": "owner_name",
              "value": "={{ $('When Executed by Another Workflow').first().json.owner_name }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1328,
        640
      ],
      "id": "2ea13c04-35e2-496f-9173-8c2bda04f425",
      "name": "[CRM] Prep Email Store Input"
    },
    {
      "parameters": {
        "workflowId": {
          "__rl": true,
          "value": "AP7QbVnt424dz8dD",
          "mode": "list",
          "cachedResultUrl": "/workflow/AP7QbVnt424dz8dD",
          "cachedResultName": "contact-memory-update"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {},
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {
          "waitForSubWorkflow": false
        }
      },
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.2,
      "position": [
        1552,
        640
      ],
      "id": "9d230dcb-4ab9-4180-876f-3440c768c8a8",
      "name": "[CRM] Call contact-email-store"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://script.googleapis.com/v1/scripts/AKfycbz1BgGEFC9SDuGHZkRPYzhmlDBp0gbbo1z6RgYNwTQdloW1csdXLYnjvll-NuwO3E7L:run",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ function: 'writeContactData', parameters: [$json] }) }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "b965d28c-21b6-4e52-b23f-69ac98076c7d",
      "name": "[CRM] Write via Apps Script",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1104,
        640
      ],
      "credentials": {
        "googleOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// LIST MODE: Group outputs by row_id, merge batches within each row, output N items\nconst allOutputs = $input.all();\n\n// Retrieve by index because LLM chain strips input fields from output\nlet buildOutputItems = [];\ntry {\n  buildOutputItems = $('Build Output Schema').all();\n} catch (e) {}\n\nlet matchColumn, textColumn;\ntry {\n  matchColumn = $('String Input').first().json.match_column || 'email';\n  textColumn = $('String Input').first().json.text_column || 'Text_to_interpret';\n} catch (e) {\n  matchColumn = 'email';\n  textColumn = 'Text_to_interpret';\n}\n\n// Group by row_id; index fallback handles pinned data (no Build Output Schema)\nconst rowGroups = {};\nfor (let i = 0; i < allOutputs.length; i++) {\n  const item = allOutputs[i];\n  const originalItem = buildOutputItems[i]?.json || {};\n  const rowId = originalItem.row_id || `row_${i}`;\n  \n  if (!rowGroups[rowId]) {\n    rowGroups[rowId] = { items: [], matchValue: originalItem.row_id, bodyCore: originalItem.body_core };\n  }\n  rowGroups[rowId].items.push(item);\n}\n\nconst results = [];\nfor (const [rowId, group] of Object.entries(rowGroups)) {\n  const items = group.items;\n  const matchValue = group.matchValue;\n  const merged = {};\n  let confidenceData = null;\n\n  for (const item of items) {\n    const output = item.json.output;\n    if (output) {\n      if (output.confidence) {\n        confidenceData = output.confidence;\n        delete output.confidence;\n      }\n      Object.assign(merged, output);\n    }\n  }\n\n  delete merged._row_id;\n\n  if (confidenceData) {\n    merged.confidence_score = confidenceData.overall || 0;\n    merged.confidence_low_fields = Array.isArray(confidenceData.low_confidence_fields) \n      ? confidenceData.low_confidence_fields.join(', ') : '';\n    merged.confidence_reasoning = confidenceData.reasoning || '';\n    merged.review_needed = (confidenceData.overall || 0) < 70;\n  }\n\n  // FIX: Write Extracted Row is hardcoded to match on 'email' column, so we must always populate it\n  if (matchValue) {\n    merged[matchColumn] = matchValue;\n    if (matchColumn !== 'email') {\n      merged.email = matchValue;\n    }\n  }\n\n  // FIX: Only set textColumn if different from matchColumn to prevent overwriting match value\n  if (group.bodyCore && textColumn !== matchColumn) {\n    merged[textColumn] = group.bodyCore;\n  }\n\n  // Dedupe extracted emails into email + more_emails fields\n  if (Array.isArray(merged.extracted_emails)) {\n    const baseEmails = matchValue ? [matchValue] : [];\n    const allEmails = [...new Set([...baseEmails, ...merged.extracted_emails].filter(Boolean))];\n    merged.email = allEmails[0];\n    merged.more_emails = allEmails.slice(1).join(', ');\n  }\n\n  // Remove observability fields (not written to sheet)\n  delete merged.confidence_score;\n  delete merged.confidence_low_fields;\n  delete merged.confidence_reasoning;\n  delete merged.review_needed;\n\n  results.push({ json: merged });\n}\n\nreturn results;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        864,
        368
      ],
      "id": "8e306dac-0e63-4e45-a98f-29364d7cd5b4",
      "name": "Merge Outputs"
    },
    {
      "parameters": {
        "jsCode": "// Outputs N_rows \u00d7 M_batches items; each carries schema batch + row context for LLM\nconst inputRows = $('String Input').all();\nconst textColumn = $('String Input').first().json.text_column;\nconst matchColumn = $('String Input').first().json.match_column || 'email';\nconst batchSize = $('String Input').first().json.batch_size;\nconst extractDepth = $('String Input').first().json.extract_depth;\nconst schemaSheetName = $('String Input').first().json.schema_sheet_name || 'Description_hig7f6';\n\n// CRM field-to-depth mapping (fields not listed default to depth 3)\nconst DEPTH_MAP = {\n  'first_name': 1, 'surname': 1, 'email': 1,\n  'last_topic': 1, 'last_being_contacted': 1, 'last_contacted': 1,\n  'more_emails': 2, 'status': 2, 'association': 2, 'groups': 2,\n  'goal_contact_frequency': 2, 'current_job': 2, 'works_at': 2\n};\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\n// Source 2: If schema exists in sheet, parse from fetched data\nif (schemaRows.length === 0) {\n  const sheetData = $('Try Fetch Schema Sheet').all();\n  if (sheetData.length > 0 && sheetData[0].json.ColumnName) {\n    schemaRows = sheetData.map(item => item.json);\n  }\n}\n\nconst schemaItems = schemaRows.map(row => ({ json: row }));\n\n// Exclude text_column - no point extracting input back into output\nconst filteredSchema = schemaItems.filter(item => item.json.ColumnName !== textColumn);\n\n// Filter fields by depth (depth 3 = all fields, no filtering)\nconst fieldsToExtract = extractDepth >= 3\n  ? filteredSchema\n  : filteredSchema.filter(item => (DEPTH_MAP[item.json.ColumnName] || 3) <= extractDepth);\n\n// Convert our simple types (str/int/list/class) to JSON Schema\nfunction buildSchemaFromBatch(batch) {\n  const properties = {};\n  const requiredFields = [];\n\n  for (const item of batch) {\n    const columnName = item.json.ColumnName;\n    const columnType = item.json.Type;\n    const columnDescr = item.json.Description;\n\n    if (columnName && columnType) {\n      if (columnType === 'class') {\n        const classValues = item.json.Classes\n          ? item.json.Classes.split(',').map(s => s.trim()).filter(Boolean)\n          : [];\n        if (classValues.length === 0) {\n          throw new Error(\n            `Schema error in schema sheet (${schemaSheetName}): column \"${columnName}\" has Type \"class\" but the \"Classes\" column is empty. ` +\n            `Fix: add comma-separated values to the \"Classes\" column for \"${columnName}\".`\n          );\n        }\n        properties[columnName] = {\n          type: \"string\",\n          description: columnDescr,\n          enum: classValues\n        };\n      } else {\n        let jsonType = columnType;\n        if (columnType === 'str') jsonType = 'string';\n        else if (columnType === 'list') jsonType = 'array';\n        else if (columnType === 'date') jsonType = 'string';\n        else if (columnType === 'int') jsonType = 'integer';\n\n        properties[columnName] = {\n          type: jsonType,\n          description: columnDescr\n        };\n\n        if (jsonType === 'array') {\n          properties[columnName].items = { type: \"string\" };\n        }\n        // Note: no format:'date' \u2014 LLM may return empty strings for missing dates\n      }\n      requiredFields.push(columnName);\n    }\n  }\n\n  // LLM self-reports extraction confidence for observability\n  properties.confidence = {\n    type: \"object\",\n    description: \"Confidence scores for extraction quality\",\n    properties: {\n      overall: { type: \"integer\", minimum: 0, maximum: 100, description: \"Weighted average confidence (0-100)\" },\n      low_confidence_fields: { type: \"array\", items: { type: \"string\" }, description: \"Fields with confidence < 70\" },\n      reasoning: { type: \"string\", description: \"Brief explanation of uncertainty\" }\n    },\n    required: [\"overall\"]\n  };\n  requiredFields.push(\"confidence\");\n\n  return { type: \"object\", properties, required: requiredFields };\n}\n\n// Reserve 3 slots for confidence sub-properties (overall, low_confidence_fields, reasoning)\nconst effectiveBatchSize = Math.max(1, batchSize - 3);\n\n// Build batches from depth-filtered fields\nconst results = [];\nfor (const row of inputRows) {\n  const rowId = row.json[matchColumn] || row.json.match_value || row.json.email;\n  const bodyCore = row.json.body_core || '';\n\n  const batches = [];\n  for (let i = 0; i < fieldsToExtract.length; i += effectiveBatchSize) {\n    batches.push(fieldsToExtract.slice(i, i + effectiveBatchSize));\n  }\n\n  for (const batch of batches) {\n    const schema = buildSchemaFromBatch(batch);\n    results.push({\n      json: {\n        schema: schema,\n        body_core: bodyCore,\n        row_id: rowId,\n        match_column: matchColumn,\n        contact_name: row.json.contact_name,\n        contact_email: row.json.contact_email,\n        subject: row.json.subject\n      }\n    });\n  }\n}\n\nreturn results;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        512,
        112
      ],
      "id": "653ec2ed-e48c-4836-991a-8203bcb76a22",
      "name": "Build Output Schema"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "17f1ff39-06cb-441e-ba35-2e83ec5a79d1",
              "name": "body_core",
              "value": "={{ $json[$json.text_column || 'Text_to_interpret'] || $json.body_core }}",
              "type": "string"
            },
            {
              "id": "a1b2c3d4-1111-2222-3333-444455556666",
              "name": "spreadsheet_id",
              "value": "={{ $json.spreadsheet_id || '' }}",
              "type": "string"
            },
            {
              "id": "c7d8e9f0-1234-5678-9abc-def012345678",
              "name": "text_column",
              "value": "={{ $json.text_column || 'Text_to_interpret' }}",
              "type": "string"
            },
            {
              "id": "a1b2c3d4-5555-6666-7777-888899990000",
              "name": "data_sheet_name",
              "value": "={{ $json.data_sheet_name || 'Sheet1' }}",
              "type": "string"
            },
            {
              "id": "a1b2c3d4-aaaa-bbbb-cccc-ddddeeeeffff",
              "name": "schema_sheet_name",
              "value": "={{ $json.schema_sheet_name || 'Description_hig7f6' }}",
              "type": "string"
            },
            {
              "id": "f1a2b3c4-1234-5678-90ab-cdef01234567",
              "name": "match_same_row",
              "value": "={{ $json.match_same_row !== false }}",
              "type": "boolean"
            },
            {
              "id": "2af3e327-ba41-41d7-818b-6608d82566d0",
              "name": "match_column",
              "value": "={{ $json.match_column || 'email' }}",
              "type": "string"
            },
            {
              "id": "match-value-generic",
              "name": "match_value",
              "value": "={{ $json[$json.match_column] || $json.contact_email }}",
              "type": "string"
            },
            {
              "id": "6d14c865-91b0-44d0-9f6b-ee4371e769c5",
              "name": "batch_size",
              "value": "={{ $json.batch_size || 7 }}",
              "type": "number"
            },
            {
              "id": "f1a2b3c4-2345-6789-0abc-def012345678",
              "name": "row_number",
              "value": "={{ $json.row_number || null }}",
              "type": "number"
            },
            {
              "id": "0eabb116-4199-4379-99c2-d93c7da8650e",
              "name": "email",
              "value": "={{ $json.contact_email }}",
              "type": "string"
            },
            {
              "id": "e5f6a7b8-c9d0-1234-5678-90abcdef0012",
              "name": "subject",
              "value": "={{ $json.subject || '' }}",
              "type": "string"
            },
            {
              "id": "d4e5f6a7-b8c9-0123-4567-890abcdef001",
              "name": "contact_name",
              "value": "={{ $json.contact_name }}",
              "type": "string"
            },
            {
              "id": "1d5f5798-24d9-46f6-871d-5b8e2ca44222",
              "name": "contact_email",
              "value": "={{ $json.contact_email }}",
              "type": "string"
            },
            {
              "id": "extract-depth",
              "name": "extract_depth",
              "value": "={{ $json.extract_depth || 3 }}",
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -416,
        128
      ],
      "id": "a39effdc-1339-4c79-91df-3adcf270491b",
      "name": "String Input",
      "notes": "body_core default: {{ $json[$json.text_column || 'Text_to_interpret'] || $json.body_core }}"
    },
    {
      "parameters": {
        "operation": "={{ $('String Input').first().json.match_same_row ? 'appendOrUpdate' : 'append' }}",
        "documentId": {
          "__rl": true,
          "value": "={{ $('String Input').first().json.spreadsheet_id }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('String Input').first().json.data_sheet_name }}",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {
            "email": "={{ $json.email }}"
          },
          "matchingColumns": [
            "email"
          ],
          "schema": [
            {
              "id": "email",
              "displayName": "email",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "handlingExtraData": "ignoreIt"
        }
      },
      "id": "6536b77a-7731-4c24-b55a-c1deb3242860",
      "name": "Write Extracted Row",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1088,
        368
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "disabled": true
    },
    {
      "parameters": {
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $('String Input').first().json.spreadsheet_id }}/values/{{ encodeURIComponent($('String Input').first().json.data_sheet_name) }}!1:1",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "options": {}
      },
      "id": "4bd29691-a7fa-450b-9af5-3a38a86ae810",
      "name": "Fetch Data Sheet Headers",
      "notes": "folder2table combines this into Fetch Data & Schema Sheets",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -208,
        128
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "={{ $('String Input').first().json.spreadsheet_id }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('String Input').first().json.schema_sheet_name }}",
          "mode": "name"
        },
        "options": {}
      },
      "id": "6162e68b-1dc3-44bb-898d-c9140732a2eb",
      "name": "Try Fetch Schema Sheet",
      "notes": "folder2table combines this into Fetch Data & Schema Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        16,
        128
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "schema-exists-condition",
              "leftValue": "={{ $json.ColumnName }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "02de666e-fd0e-433a-8b04-3bed444b3027",
      "name": "IF: Schema Exists?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        240,
        128
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=You are a database schema expert. Given these column names from a spreadsheet:\n{{ $('Fetch Data Sheet Headers').first().json.values[0] }}\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": "9cc57e33-c200-4ebe-8397-cabe776f326c",
      "name": "LLM: Generate Schema",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        -112,
        368
      ]
    },
    {
      "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": "4e9fffd7-077d-4d37-8674-894c431d500f",
      "name": "Schema Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        32,
        544
      ]
    },
    {
      "parameters": {
        "model": "openai/gpt-oss-120b",
        "options": {}
      },
      "id": "d9e7f1b9-0f28-4a12-825f-29d83564e1cc",
      "name": "Schema LLM",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "typeVersion": 1,
      "position": [
        -112,
        544
      ],
      "credentials": {
        "groqApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=You are a precise data extraction specialist. Your task is to analyze the provided text and extract specific information into the exact JSON structure required.\n\n## SOURCE DATA:\n{{ ($json.contact_name || $json.contact_email || $json.subject) ? '<context>\\nContact: ' + ($json.contact_name || '') + '\\nEmail: ' + ($json.contact_email || '') + '\\nSubject: ' + ($json.subject || '') + '\\n</context>\\n\\n' : '' }}<source_text>\n{{ $json.body_core }}\n</source_text>\n\n\n## CRITICAL INSTRUCTIONS:\n1. **ACCURACY FIRST**: Extract information exactly as stated in the source - do not estimate, round, or modify values\n2. **DEFAULT VALUES**: If information is not found, use an empty string \"\"\n3. **CLASSIFICATION CATEGORIES**: For \"enums\" use the most appropriate option or first option if unclear\n\n## CONFIDENCE SCORING:\nFor each extracted field, assess your confidence (0-100):\n- 90-100: Explicitly stated, unambiguous\n- 70-89: Clear but requires interpretation\n- 50-69: Ambiguous, multiple interpretations possible\n- 0-49: Missing, contradictory, or speculative\n\nInclude a \"confidence\" object in your response with:\n- \"overall\": weighted average of all field confidences (integer 0-100)\n- \"low_confidence_fields\": array of field names with confidence < 70\n- \"reasoning\": brief explanation of any uncertainty (max 100 chars)\n\n## QUALITY CHECK:\n- Double-check that all required fields are present\n- Be honest about uncertainty - do not inflate confidence scores\n\nStart here.",
        "hasOutputParser": true,
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        512,
        368
      ],
      "id": "b94e8302-4526-4bc2-8161-abe8208d9494",
      "name": "Extract Data from String",
      "alwaysOutputData": false,
      "retryOnFail": true,
      "maxTries": 2
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "={{ JSON.stringify($json.schema, null, \" \") }}",
        "options": {
          "autoFix": true
        }
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        656,
        544
      ],
      "id": "e327d04c-716b-4633-8d8c-53bd4d0be9bc",
      "name": "Dynamic Output Parser"
    },
    {
      "parameters": {
        "model": "openai/gpt-oss-120b",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "typeVersion": 1,
      "position": [
        512,
        544
      ],
      "id": "678e935f-f073-4af7-99d9-826d9ac899a7",
      "name": "LLM Processor",
      "notesInFlow": false,
      "credentials": {
        "groqApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -864,
        128
      ],
      "id": "9af42ee7-9e8a-4626-99a2-988afbea0e5d",
      "name": "Manual Trigger"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "YOUR_TEST_SPREADSHEET_ID",
          "mode": "list",
          "cachedResultName": "Your Test Sheet",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_TEST_SPREADSHEET_ID/edit"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "Sheet1",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_TEST_SPREADSHEET_ID/edit"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -640,
        128
      ],
      "id": "1d25ead0-0e4a-4426-bd11-7f78ea8d1ab2",
      "name": "Get row(s) in sheet",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "inputSource": "passthrough"
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        -640,
        -64
      ],
      "id": "ca4b481f-0727-4965-bc3a-afe0890e6859",
      "name": "When Executed by Another Workflow"
    },
    {
      "parameters": {
        "content": "## Schema Auto-Creation\nOn first run, if `Description_hig7f6` 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.\n\n---\nOptional:\nConfidence scoring via `confidence` field. See `../docs/observability-through-llm-confidence-estimate.md",
        "height": 328,
        "width": 420
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1136,
        0
      ],
      "id": "09eabe66-e68d-4cf3-ac73-bb6fd82d4273",
      "name": "Sticky Note - Schema Info"
    },
    {
      "parameters": {
        "content": "### If stops here\n\nYour `Description_hig7f6` sheet exists but has faulty syntax.\n\n**Fix:** Delete the sheet from Google Sheets and re-run to regenerate it.",
        "height": 80,
        "width": 150
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -16,
        96
      ],
      "id": "3a251222-bbdc-4a3d-8cdc-47780f170ecf",
      "name": "Sticky Note - Troubleshoot"
    },
    {
      "parameters": {
        "content": "## Mode B: Email-CRM Setup\n[apps-script-setup](https://github.com/runfish5/micro-services/blob/main/projects/n8n/02_smart-table-fill/docs/apps-script-execution-api-setup.md) \u00b7 [email-crm-guide](https://github.com/runfish5/micro-services/blob/main/projects/n8n/02_smart-table-fill/docs/email-crm-guide.md) \u00b7 [troubleshooting](https://github.com/runfish5/micro-services/blob/main/projects/n8n/troubleshooting.md#apps-script-execution-api)",
        "height": 220,
        "width": 668
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1056,
        560
      ],
      "id": "261acaf4-0f5b-4434-8bc6-c8c0b2f0fe68",
      "name": "Sticky - CRM Setup"
    },
    {
      "parameters": {
        "content": "### If stops here\n\n403 after GCP project change?\nSee [troubleshooting](https://github.com/runfish5/micro-services/blob/main/projects/n8n/troubleshooting.md#403-permission-errors-incl-gcp-project-migration)",
        "height": 80,
        "width": 150
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1080,
        608
      ],
      "id": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f",
      "name": "Sticky - Credential Recovery"
    },
    {
      "parameters": {
        "content": "## Mode A: Smart Table Fill\nActive by default. Writes extracted data directly to Google Sheets.\n\nToggle `Operation` to `Append or Update Row` to auto-create new entries (vs `Update Row` = fails silently if not found)",
        "height": 176,
        "width": 424
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1296,
        368
      ],
      "id": "780dbcf9-d414-4cbe-ba20-4230b689ed4f",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "### If stops here\n\n- a. You must delete the \"Description_hig7f6\" google sheet sheet\n\n- b. You must put column names into Google Sheets 'Sheet1' first row.",
        "height": 80,
        "width": 150
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        208,
        336
      ],
      "id": "a8ced62a-68d5-47ca-8df4-f5f089b7e3f9",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $('String Input').first().json.spreadsheet_id }}:batchUpdate",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ (() => { const id = Date.now() % 1000000; const name = $('String Input').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": "8375961a-8d9c-4cfb-8f9e-4ea3cdae33f8",
      "name": "Create & Write Schema Sheet",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        240,
        368
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "content": "### If it stops here\n\nThe workflow expects your data in a sheet named \"Sheet1\" by default.\n\n**Fix:** Update `data_sheet_name` in the \"String Input\" node to match your actual sheet name.",
        "height": 80,
        "width": 150
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -256,
        96
      ],
      "id": "89bfad2f-83d6-43b7-b01f-22eccaddd1db",
      "name": "Sticky Note - Troubleshoot1"
    },
    {
      "parameters": {
        "content": "### String Input config\nSet these fields here:\n- `spreadsheet_id`: from URL `.../d/`**THIS_PART**`/edit`\n- `data_sheet_name`: `{{ $json.data_sheet_name || 'Sheet1' || 'Entries' }}`\n- `batch_size`: **7**",
        "height": 170,
        "width": 190,
        "color": 3
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -464,
        96
      ],
      "id": "d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a",
      "name": "Sticky - Spreadsheet ID"
    },
    {
      "parameters": {
        "content": "### If stops here\n\nRate Limit Errors? If you're hitting API rate limits (e.g., Groq free tier 5 requests/min), see:\n[troubleshooting.md](https://github.com/runfish5/micro-services/blob/main/projects/n8n/troubleshooting.md)",
        "height": 80,
        "width": 150
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        488,
        327
      ],
      "id": "d109b025-adcc-44fe-966a-8dccdac5da90",
      "name": "Sticky Note - Rate Limit Help"
    },
    {
      "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": [
        488,
        80
      ],
      "id": "a1b2c3d4-empty-class-guard-stf",
      "name": "Sticky Note - Empty Class Guard"
    }
  ],
  "connections": {
    "[CRM] Prep Email Store Input": {
      "main": [
        [
          {
            "node": "[CRM] Call contact-email-store",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[CRM] Write via Apps Script": {
      "main": [
        [
          {
            "node": "[CRM] Prep Email Store Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Outputs": {
      "main": [
        [
          {
            "node": "[CRM] Write via Apps Script",
            "type": "main",
            "index": 0
          },
          {
            "node": "Write Extracted Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Output Schema": {
      "main": [
        [
          {
            "node": "Extract Data from String",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "String Input": {
      "main": [
        [
          {
            "node": "Fetch Data Sheet Headers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Data Sheet Headers": {
      "main": [
        [
          {
            "node": "Try Fetch Schema Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try Fetch Schema Sheet": {
      "main": [
        [
          {
            "node": "IF: Schema Exists?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Schema Exists?": {
      "main": [
        [
          {
            "node": "Build Output Schema",
            "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 Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "LLM: Generate Schema",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Schema LLM": {
      "ai_languageModel": [
        [
          {
            "node": "LLM: Generate Schema",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Extract Data from String": {
      "main": [
        [
          {
            "node": "Merge Outputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Dynamic Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Extract Data from String",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "LLM Processor": {
      "ai_languageModel": [
        [
          {
            "node": "Extract Data from String",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Get row(s) in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get row(s) in sheet": {
      "main": [
        [
          {
            "node": "String Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "String Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create & Write Schema Sheet": {
      "main": [
        [
          {
            "node": "Build Output Schema",
            "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

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

Source: https://github.com/runfish5/micro-services/blob/main/projects/n8n/02_smart-table-fill/workflows/smart-table-fill.n8n.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-Folder2Table. Uses executeWorkflowTrigger, httpRequest, chainLlm, lmChatGroq. Event-driven trigger; 26 nodes.

Execute Workflow Trigger, HTTP Request, Chain Llm +4
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

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

HTTP Request, OpenAI Chat, Chain Llm +3
Data & Sheets

Splitout Code. Uses lmChatOpenRouter, outputParserStructured, httpRequest, stickyNote. Event-driven trigger; 11 nodes.

OpenRouter Chat, Output Parser Structured, HTTP Request +2