AutomationFlowsWeb Scraping › [fc Sub] Extract Structured Data

[fc Sub] Extract Structured Data

[FC Sub] Extract Structured Data. Uses executeWorkflowTrigger, httpRequest, postgres. Event-driven trigger; 12 nodes.

Event trigger★★★★☆ complexity12 nodesExecute Workflow TriggerHTTP RequestPostgres
Web Scraping Trigger: Event Nodes: 12 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": "[FC Sub] Extract Structured Data",
  "settings": {
    "executionOrder": "v1"
  },
  "nodes": [
    {
      "parameters": {
        "content": "## [FC Sub] Extract Structured Data\n**Purpose:** LLM-powered structured extraction via Firecrawl `/v1/extract`. Takes an array of URLs plus an optional prompt/schema and returns JSON that conforms to the schema.\n\n**Called by:** main agent's `extract_data` tool.\n\n**Inputs:**\n- `urls[]` (required) \u2014 pages to extract from\n- `prompt?` \u2014 natural-language description of what to extract (required if no schema)\n- `schema?` \u2014 JSON Schema object defining the fields\n- `session_id?`\n\n**Credit cost:** 5 credits per URL extracted (Firecrawl premium op).\n\n**Validation:** If a schema is provided, the response is validated with a lightweight JSON Schema check (type + required fields). `schema_valid` and `validation_errors` are returned so the agent can decide whether to retry.\n\n**Flow:** Trigger \u2192 Prep \u2192 POST `/v1/extract` \u2192 Wait 20s \u2192 Poll \u2192 Shape + Validate \u2192 Ledger \u2192 Return.\n\n**Credentials:** Firecrawl API, Postgres RW.",
        "height": 580,
        "width": 640,
        "color": 6
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -620
      ],
      "id": "sticky-fc-extract",
      "name": "README"
    },
    {
      "parameters": {
        "inputSource": "passthrough"
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        0
      ],
      "id": "fc-ext-trigger",
      "name": "When Executed by Another Workflow"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst q = (input.query && typeof input.query === 'object') ? input.query : {};\nconst pick = (k, def) => {\n  if (input[k] !== undefined) return input[k];\n  if (q[k] !== undefined) return q[k];\n  return def;\n};\nconst parseIfString = (v, fb) => {\n  if (v === undefined || v === null) return fb;\n  if (typeof v !== 'string') return v;\n  try { return JSON.parse(v); } catch { return fb; }\n};\n\nlet urls = parseIfString(pick('urls'), null);\nif (!Array.isArray(urls)) {\n  const single = pick('url');\n  if (typeof single === 'string') urls = [single];\n}\nif (!Array.isArray(urls) || urls.length === 0) {\n  throw new Error('extract_data requires `urls` (array) or `url` (single string).');\n}\nurls = urls\n  .map(u => (typeof u === 'string' ? u.trim() : ''))\n  .filter(u => u.length > 0)\n  .map(u => /^https?:\\/\\//i.test(u) ? u : 'https://' + u);\nif (urls.length > 10) urls = urls.slice(0, 10); // hard cap \u2014 extract is expensive (5 credits each)\n\nconst prompt = (pick('prompt') || '').toString();\nconst schema = parseIfString(pick('schema'), null);\n\nif (!prompt && !schema) {\n  throw new Error('extract_data requires either `prompt` (natural-language goal) or `schema` (JSON Schema).');\n}\n\nconst session_id = (pick('session_id') || $execution.id || 'no-session').toString();\n\nreturn [{ json: {\n  urls,\n  url_count: urls.length,\n  prompt,\n  schema,\n  session_id,\n  execution_id: $execution.id\n} }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "id": "fc-ext-prep",
      "name": "Prep Input"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.firecrawl.dev/v1/extract",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"urls\": {{ JSON.stringify($json.urls) }}\n  {{ $json.prompt ? (', \"prompt\": ' + JSON.stringify($json.prompt)) : '' }}\n  {{ $json.schema ? (', \"schema\": ' + JSON.stringify($json.schema)) : '' }}\n}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 60000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        0
      ],
      "id": "fc-ext-start",
      "name": "Start Extract Job"
    },
    {
      "parameters": {
        "amount": 20,
        "unit": "seconds"
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        660,
        0
      ],
      "id": "fc-ext-wait1",
      "name": "Wait 20s"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://api.firecrawl.dev/v1/extract/{{ $('Start Extract Job').first().json.id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 30000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        880,
        0
      ],
      "id": "fc-ext-poll1",
      "name": "Poll Status (20s)"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "ext-status-1",
              "leftValue": "={{ $json.status }}",
              "rightValue": "completed",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1100,
        0
      ],
      "id": "fc-ext-if-done",
      "name": "IF Completed"
    },
    {
      "parameters": {
        "amount": 30,
        "unit": "seconds"
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1320,
        200
      ],
      "id": "fc-ext-wait2",
      "name": "Wait 30s"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://api.firecrawl.dev/v1/extract/{{ $('Start Extract Job').first().json.id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 30000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1540,
        200
      ],
      "id": "fc-ext-poll2",
      "name": "Poll Status (50s)"
    },
    {
      "parameters": {
        "jsCode": "const prep = $('Prep Input').first().json;\nconst start = $('Start Extract Job').first().json;\nconst resp = $input.first().json;\n\nconst status = resp.status || 'unknown';\nconst complete = status === 'completed';\nconst data = resp.data || null;\n\n// Lightweight JSON Schema validation (type + required fields only)\nfunction validate(data, schema) {\n  const errors = [];\n  if (!schema || typeof schema !== 'object') return { valid: true, errors };\n\n  const checkType = (val, type) => {\n    if (type === 'array') return Array.isArray(val);\n    if (type === 'object') return val !== null && typeof val === 'object' && !Array.isArray(val);\n    return typeof val === type;\n  };\n\n  if (schema.type && !checkType(data, schema.type)) {\n    errors.push(`Root value expected type ${schema.type}, got ${Array.isArray(data) ? 'array' : typeof data}`);\n  }\n  if (schema.type === 'object' && Array.isArray(schema.required)) {\n    for (const f of schema.required) {\n      if (data === null || data === undefined || !(f in data)) {\n        errors.push(`Missing required field: ${f}`);\n      }\n    }\n  }\n  if (schema.type === 'object' && schema.properties && data && typeof data === 'object') {\n    for (const [f, sub] of Object.entries(schema.properties)) {\n      if (f in data && sub.type && !checkType(data[f], sub.type)) {\n        errors.push(`Field ${f} expected type ${sub.type}, got ${Array.isArray(data[f]) ? 'array' : typeof data[f]}`);\n      }\n    }\n  }\n  return { valid: errors.length === 0, errors };\n}\n\nconst validation = validate(data, prep.schema);\nconst credits_used = complete ? (prep.url_count * 5) : 0;\n\nreturn [{ json: {\n  status,\n  complete,\n  job_id: start.id || null,\n  data,\n  schema_valid: validation.valid,\n  validation_errors: validation.errors,\n  urls: prep.urls,\n  url_count: prep.url_count,\n  credits_used,\n  note: complete ? undefined : `Extract job still running after ~50s (status: ${status}). Try again or simplify the prompt/schema.`,\n  session_id: prep.session_id,\n  execution_id: prep.execution_id\n} }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        100
      ],
      "id": "fc-ext-shape",
      "name": "Shape + Validate"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO public.firecrawl_credit_ledger (session_id, execution_id, operation, credits_used, status, metadata)\nVALUES ($1, $2, 'extract', $3, $4, $5::jsonb);",
        "options": {
          "queryReplacement": "={{ $json.session_id }}, {{ $json.execution_id }}, {{ $json.credits_used }}, {{ $json.complete ? 'ok' : 'partial' }}, {{ JSON.stringify({ status: $json.status, url_count: $json.url_count, schema_valid: $json.schema_valid, validation_errors: $json.validation_errors, job_id: $json.job_id }) }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1980,
        100
      ],
      "id": "fc-ext-ledger",
      "name": "Log Credit Ledger"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e1",
              "name": "status",
              "value": "={{ $('Shape + Validate').first().json.status }}",
              "type": "string"
            },
            {
              "id": "e2",
              "name": "complete",
              "value": "={{ $('Shape + Validate').first().json.complete }}",
              "type": "boolean"
            },
            {
              "id": "e3",
              "name": "data",
              "value": "={{ $('Shape + Validate').first().json.data }}",
              "type": "object"
            },
            {
              "id": "e4",
              "name": "schema_valid",
              "value": "={{ $('Shape + Validate').first().json.schema_valid }}",
              "type": "boolean"
            },
            {
              "id": "e5",
              "name": "validation_errors",
              "value": "={{ $('Shape + Validate').first().json.validation_errors }}",
              "type": "array"
            },
            {
              "id": "e6",
              "name": "credits_used",
              "value": "={{ $('Shape + Validate').first().json.credits_used }}",
              "type": "number"
            },
            {
              "id": "e7",
              "name": "note",
              "value": "={{ $('Shape + Validate').first().json.note }}",
              "type": "string"
            },
            {
              "id": "e8",
              "name": "job_id",
              "value": "={{ $('Shape + Validate').first().json.job_id }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2200,
        100
      ],
      "id": "fc-ext-return",
      "name": "Return Extract"
    }
  ],
  "connections": {
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "Prep Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prep Input": {
      "main": [
        [
          {
            "node": "Start Extract Job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start Extract Job": {
      "main": [
        [
          {
            "node": "Wait 20s",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 20s": {
      "main": [
        [
          {
            "node": "Poll Status (20s)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll Status (20s)": {
      "main": [
        [
          {
            "node": "IF Completed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Completed": {
      "main": [
        [
          {
            "node": "Shape + Validate",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 30s",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 30s": {
      "main": [
        [
          {
            "node": "Poll Status (50s)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll Status (50s)": {
      "main": [
        [
          {
            "node": "Shape + Validate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Shape + Validate": {
      "main": [
        [
          {
            "node": "Log Credit Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Credit Ledger": {
      "main": [
        [
          {
            "node": "Return Extract",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

[FC Sub] Extract Structured Data. Uses executeWorkflowTrigger, httpRequest, postgres. Event-driven trigger; 12 nodes.

Source: https://github.com/MinaSaad1/n8n-firecrawl-web-crawler-agent/blob/main/workflows/08-sub-extract-structured.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

[FC Sub] Scrape URL with 24h Cache. Uses executeWorkflowTrigger, postgres, httpRequest. Event-driven trigger; 14 nodes.

Execute Workflow Trigger, Postgres, HTTP Request
Web Scraping

[FC Sub] Crawl Site (Bounded). Uses executeWorkflowTrigger, httpRequest, postgres. Event-driven trigger; 12 nodes.

Execute Workflow Trigger, HTTP Request, Postgres
Web Scraping

[FC Sub] Batch Scrape URLs. Uses executeWorkflowTrigger, httpRequest, postgres. Event-driven trigger; 12 nodes.

Execute Workflow Trigger, HTTP Request, Postgres
Web Scraping

[FC Sub] Search the Web. Uses executeWorkflowTrigger, httpRequest, postgres. Event-driven trigger; 7 nodes.

Execute Workflow Trigger, HTTP Request, Postgres
Web Scraping

[FC Sub] Map a Website. Uses executeWorkflowTrigger, httpRequest, postgres. Event-driven trigger; 7 nodes.

Execute Workflow Trigger, HTTP Request, Postgres