AutomationFlowsWeb Scraping › [dev] Post-call / Llm-extraction-engine

[dev] Post-call / Llm-extraction-engine

[DEV] post-call / llm-extraction-engine. Uses executeWorkflowTrigger, httpRequest. Event-driven trigger; 9 nodes.

Event trigger★★★★☆ complexity9 nodesExecute Workflow TriggerHTTP Request
Web Scraping Trigger: Event Nodes: 9 Complexity: ★★★★☆ Added:

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

The workflow JSON

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

Download .json
{
  "name": "[DEV] post-call / llm-extraction-engine",
  "description": null,
  "active": true,
  "nodes": [
    {
      "id": "trigger",
      "name": "Execute Workflow Trigger",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        300
      ],
      "parameters": {
        "inputSource": "passthrough"
      }
    },
    {
      "id": "validate",
      "name": "Validate Inputs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const item = $input.first().json;\nconst { transcript, agent_system_prompt, extraction_config, metadata } = item;\nconst errors = [];\n\nif (!transcript || typeof transcript !== 'string' || transcript.trim().length === 0) {\n  errors.push('transcript is required and must be a non-empty string');\n}\nif (!agent_system_prompt || typeof agent_system_prompt !== 'string') {\n  errors.push('agent_system_prompt is required');\n}\nif (!extraction_config || !Array.isArray(extraction_config.categories) || extraction_config.categories.length === 0) {\n  errors.push('extraction_config.categories must be a non-empty array');\n} else {\n  for (const cat of extraction_config.categories) {\n    if (!cat.category_id || !Array.isArray(cat.fields) || cat.fields.length === 0) {\n      errors.push(`Category '${cat.category_id || 'unknown'}' must have category_id and non-empty fields`);\n    }\n  }\n}\n\nconst extraction_id = 'ext_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);\n\nif (errors.length > 0) {\n  return [{ json: { valid: false, extraction_id, errors } }];\n}\n\nreturn [{ json: { valid: true, extraction_id, transcript, agent_system_prompt, extraction_config, metadata: metadata || {} } }];"
      }
    },
    {
      "id": "route_valid",
      "name": "Check Valid",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        440,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "valid-check",
              "leftValue": "={{ $json.valid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "error_response",
      "name": "Error Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        180
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const { extraction_id, errors } = $input.first().json;\nreturn [{ json: {\n  extraction_id,\n  timestamp: new Date().toISOString(),\n  model: 'gemini-3-pro',\n  categories_processed: 0,\n  fields: [],\n  errors: errors.map(e => ({ type: 'validation', message: e }))\n} }];"
      }
    },
    {
      "id": "split_cats",
      "name": "Split Categories",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        400
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const { extraction_id, transcript, agent_system_prompt, extraction_config, metadata, google_api_key } = $input.first().json;\nconst global_context = extraction_config.global_context || {};\nreturn extraction_config.categories.map(category => ({\n  json: { extraction_id, transcript, agent_system_prompt, category, global_context, google_api_key }\n}));"
      }
    },
    {
      "id": "build_prompt",
      "name": "Build 5-Component Prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        400
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const { category, transcript, agent_system_prompt, global_context, google_api_key } = $input.item.json;\n\nfunction inferStrictness(field) {\n  if (field.strictness) return field.strictness;\n  if (['boolean','phone','email'].includes(field.type)) return 'high';\n  if (field.validation && field.validation.pattern) return 'high';\n  if (field.type === 'enum' && field.values && field.values.length <= 5) return 'high';\n  if (field.type === 'enum') return 'medium';\n  if (field.type === 'string' && field.required) return 'medium';\n  if (/summary|notes|description/i.test(field.field_id)) return 'low';\n  if (field.type === 'string' && !field.required) return 'low';\n  return 'medium';\n}\n\nconst compA = '== TRANSCRIPT ==\\n' + transcript;\nconst compB = '== CONTEXT ==\\n' + agent_system_prompt;\n\nconst schema = {};\nfor (const f of category.fields) {\n  if (f.type === 'boolean') schema[f.field_id] = 'boolean | null';\n  else if (f.type === 'enum') schema[f.field_id] = (f.values || []).join(' | ');\n  else if (f.type === 'number') schema[f.field_id] = 'number | null';\n  else schema[f.field_id] = 'string | null';\n}\nconst compC = '== RESPONSE SCHEMA ==\\nReturn a JSON object with exactly these keys:\\n' + JSON.stringify(schema, null, 2) + '\\n\\nAlso include a \\\"_rationale\\\" object with the same keys, where each value explains your reasoning.\\nAlso include a \\\"_confidence\\\" object with the same keys, where each value is a number 0.0-1.0.';\n\nconst fieldLines = category.fields.map(function(f) {\n  var prompt = f.prompt || 'Extract the ' + f.field_id + ' from the transcript.';\n  var s = inferStrictness(f);\n  var line = '- **' + f.field_id + '** (' + f.type + '): ' + prompt;\n  if (f.type === 'enum' && f.values) line += '\\n  Allowed values: ' + f.values.join(', ');\n  if (f.required) line += '\\n  Required. Default: ' + JSON.stringify(f.default_value !== undefined ? f.default_value : null);\n  line += '\\n  Strictness: ' + s;\n  return line;\n}).join('\\n\\n');\nconst compD = '== FIELD INSTRUCTIONS ==\\n' + fieldLines;\n\nconst rules = category.context_rules || {};\nvar compE = '== EXTRACTION RULES ==\\nDefault strictness: ' + (rules.default_strictness || 'medium') + '\\nRationale required: ' + (rules.require_rationale !== undefined ? rules.require_rationale : true) + '\\nNull behavior: ' + (rules.null_behavior || 'return_null_with_rationale') + '\\n\\nStrictness guide:\\n- HIGH: Exact match only. Return null if ambiguous. Enums must match exactly.\\n- MEDIUM: Best-effort extraction. Use \\\"unknown\\\" if unsure.\\n- LOW: Flexible summarization. Creative interpretation OK.\\n\\nConfidence scoring: 0.0 = pure guess, 0.5 = some evidence, 0.8 = strong evidence, 1.0 = explicitly stated.\\nIf a field is not mentioned in the transcript at all, return null with confidence 0.0.';\nif (global_context && global_context.agent_identity) compE += '\\nAgent identity: ' + global_context.agent_identity;\nif (global_context && global_context.business_domain) compE += '\\nBusiness domain: ' + global_context.business_domain;\n\nconst fullPrompt = 'You are a structured data extraction engine. Extract fields from the transcript below.\\n\\n' + compA + '\\n\\n' + compB + '\\n\\n' + compC + '\\n\\n' + compD + '\\n\\n' + compE;\n\nreturn { json: { prompt: fullPrompt, category_id: category.category_id, fields: category.fields, context_rules: category.context_rules || {}, google_api_key: google_api_key } };"
      }
    },
    {
      "id": "call_gemini",
      "name": "Call Gemini 3 Pro",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        400
      ],
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpQueryAuth",
        "sendQuery": false,
        "sendHeaders": false,
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ contents: [{ parts: [{ text: $json.prompt }] }], generationConfig: { temperature: 0.1, responseMimeType: 'application/json' } }) }}",
        "options": {
          "response": {
            "response": {
              "fullResponse": true
            }
          },
          "timeout": 30000
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "parse_validate",
      "name": "Parse and Validate Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        400
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const input = $input.item.json;\nconst category_id = $('Build 5-Component Prompt').item.json.category_id;\nconst fields = $('Build 5-Component Prompt').item.json.fields;\n\nlet parsed;\ntry {\n  const body = input.body || input;\n  const text = body && body.candidates && body.candidates[0] && body.candidates[0].content && body.candidates[0].content.parts && body.candidates[0].content.parts[0] && body.candidates[0].content.parts[0].text;\n  if (!text) throw new Error('No text in Gemini response');\n  parsed = JSON.parse(text);\n} catch (e) {\n  return { json: {\n    category_id: category_id,\n    envelopes: [],\n    error: { type: 'parse_error', category: category_id, message: 'Failed to parse Gemini response: ' + e.message }\n  } };\n}\n\nconst E164 = /^\\+[1-9]\\d{1,14}$/;\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nfunction repair(field, raw) {\n  if (raw === null || raw === undefined) return null;\n  if (field.type === 'boolean') {\n    if (typeof raw === 'boolean') return raw;\n    if (typeof raw === 'string') {\n      if (raw.toLowerCase() === 'true') return true;\n      if (raw.toLowerCase() === 'false') return false;\n    }\n    return null;\n  }\n  if (field.type === 'enum' && field.values) {\n    const str = String(raw).trim();\n    const exact = field.values.find(function(v) { return v === str; });\n    if (exact) return exact;\n    const ci = field.values.find(function(v) { return v.toLowerCase() === str.toLowerCase(); });\n    if (ci) return ci;\n    return str;\n  }\n  if (typeof raw === 'string') return raw.trim();\n  return raw;\n}\n\nfunction validate(field, value) {\n  if (value === null || value === undefined) return !field.required;\n  switch (field.type) {\n    case 'boolean': return typeof value === 'boolean';\n    case 'phone': return typeof value === 'string' && E164.test(value);\n    case 'email': return typeof value === 'string' && EMAIL.test(value);\n    case 'enum': return typeof value === 'string' && (!field.values || field.values.includes(value));\n    case 'number': return typeof value === 'number' && !isNaN(value);\n    case 'string': return typeof value === 'string' && value.length > 0;\n    default: return true;\n  }\n}\n\nfunction inferStrictness(field) {\n  if (field.strictness) return field.strictness;\n  if (['boolean','phone','email'].includes(field.type)) return 'high';\n  if (field.type === 'enum' && field.values && field.values.length <= 5) return 'high';\n  if (field.type === 'enum') return 'medium';\n  if (field.type === 'string' && field.required) return 'medium';\n  if (/summary|notes|description/i.test(field.field_id)) return 'low';\n  if (field.type === 'string' && !field.required) return 'low';\n  return 'medium';\n}\n\nconst rationales = parsed._rationale || {};\nconst confidences = parsed._confidence || {};\nconst envelopes = [];\n\nfor (const field of fields) {\n  const raw = parsed[field.field_id];\n  const value = repair(field, raw);\n  const valid = validate(field, value);\n  const finalValue = (!valid && field.required && field.default_value !== undefined) ? field.default_value : value;\n\n  envelopes.push({\n    category: category_id,\n    field_id: field.field_id,\n    value: finalValue,\n    rationale: rationales[field.field_id] || null,\n    confidence: typeof confidences[field.field_id] === 'number' ? confidences[field.field_id] : 0,\n    strictness_applied: inferStrictness(field),\n    validation_passed: valid || (finalValue !== value)\n  });\n}\n\nreturn { json: { category_id: category_id, envelopes: envelopes, error: null } };"
      }
    },
    {
      "id": "aggregate",
      "name": "Aggregate Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        400
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nlet extraction_id = 'ext_unknown';\ntry { extraction_id = $('Split Categories').first().json.extraction_id; } catch(e) {}\n\nconst allEnvelopes = [];\nconst allErrors = [];\nlet categoriesProcessed = 0;\n\nfor (const item of items) {\n  const data = item.json;\n  if (data.envelopes && Array.isArray(data.envelopes)) {\n    allEnvelopes.push(...data.envelopes);\n    categoriesProcessed++;\n  }\n  if (data.error) {\n    allErrors.push(data.error);\n  }\n}\n\nconst seen = new Set();\nconst deduped = allEnvelopes.filter(function(e) {\n  const key = e.category + ':' + e.field_id;\n  if (seen.has(key)) return false;\n  seen.add(key);\n  return true;\n});\n\ndeduped.sort(function(a, b) { return a.category.localeCompare(b.category) || a.field_id.localeCompare(b.field_id); });\n\nreturn [{ json: {\n  extraction_id: extraction_id,\n  timestamp: new Date().toISOString(),\n  model: 'gemini-3-pro',\n  categories_processed: categoriesProcessed,\n  fields: deduped,\n  errors: allErrors\n} }];"
      }
    }
  ],
  "connections": {
    "Execute Workflow Trigger": {
      "main": [
        [
          {
            "node": "Validate Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Inputs": {
      "main": [
        [
          {
            "node": "Check Valid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Valid": {
      "main": [
        [
          {
            "node": "Split Categories",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Categories": {
      "main": [
        [
          {
            "node": "Build 5-Component Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build 5-Component Prompt": {
      "main": [
        [
          {
            "node": "Call Gemini 3 Pro",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Gemini 3 Pro": {
      "main": [
        [
          {
            "node": "Parse and Validate Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse and Validate Response": {
      "main": [
        [
          {
            "node": "Aggregate Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false
  },
  "tags": []
}
Pro

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

About this workflow

[DEV] post-call / llm-extraction-engine. Uses executeWorkflowTrigger, httpRequest. Event-driven trigger; 9 nodes.

Source: https://github.com/wranngle/n8n_showcase/blob/main/workflows/post-call/llm-extraction-engine.json — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

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

Web Scraping

02_LLM_Pipeline v1.0. Uses executeWorkflowTrigger, httpRequest, seaTable. Event-driven trigger; 65 nodes.

Execute Workflow Trigger, HTTP Request, Sea Table
Web Scraping

This template is a powerful, reusable utility for managing stateful, long-running processes. It allows a main workflow to be paused indefinitely at "checkpoints" and then be resumed by external, async

HTTP Request, Execute Workflow Trigger
Web Scraping

Upload files from any source to your account Kommo or AmoCRM with a simple and reusable workflow. It can split a large file into small ones and upload chunks. Works for Kommo and amoCRM There are 3 re

HTTP Request, Execute Workflow Trigger, Stop And Error
Web Scraping

Remixed Backup your workflows to GitHub from Solomon's work. Check out his templates.

HTTP Request, GitHub, Execute Workflow Trigger +1
Web Scraping

Remixed Backup your workflows to GitHub from Solomon's work. Check out his templates.

Execute Workflow Trigger, HTTP Request, GitHub