{
  "name": "Firecrawl Web Crawler Agent",
  "settings": {
    "executionOrder": "v1"
  },
  "nodes": [
    {
      "parameters": {
        "content": "## Firecrawl Web Crawler Agent\n**What it does:** A conversational web research agent with full Firecrawl tool coverage (scrape, search, map, crawl, batch, extract, autopilot, browser sandbox) plus a Postgres credit ledger and 24h scrape cache.\n\n**Architecture:** Chat Trigger \u2192 LangChain Tools Agent (Claude Sonnet 4.6 via OpenRouter) \u2192 10 tools wired into 9 sub-workflows + a `think` scratchpad.\n\n### Required credentials\n1. **Firecrawl API** \u2014 HTTP Header Auth with `Authorization: Bearer fc-...`. Attach to every sub-workflow's HTTP Request node.\n2. **OpenRouter API** \u2014 attach to the `OpenRouter Chat Model` node (or swap for any LangChain-supported LLM).\n3. **Postgres** (writable) \u2014 attach to `Postgres Chat Memory` and every Postgres node inside the sub-workflows. Use the `firecrawl_agent_rw` role created by `sql/01-setup.sql`.\n\n### Sub-workflows required (import first, then re-link IDs)\n- `[FC Sub] Scrape URL with 24h Cache` \u2192 `scrape_url`\n- `[FC Sub] Search the Web` \u2192 `search_web`\n- `[FC Sub] Map a Website` \u2192 `map_site`\n- `[FC Sub] Crawl Site (Bounded)` \u2192 `crawl_site`\n- `[FC Sub] Batch Scrape URLs` \u2192 `batch_scrape`\n- `[FC Sub] Extract Structured Data` \u2192 `extract_data`\n- `[FC Sub] Firecrawl Autopilot Agent` \u2192 `firecrawl_autopilot`\n- `[FC Sub] Browser Sandbox Session` \u2192 `browser_session`\n- `[FC Sub] Check Credit Spend` \u2192 `check_credits`\n\n### Before activating\n1. Run `sql/01-setup.sql` in your Postgres.\n2. Create the 3 credentials above.\n3. Import sub-workflows 03-11, then open this workflow and re-select each tool's sub-workflow from the dropdown.\n4. Optional: INSERT into `firecrawl_allowed_domains` to enforce a strict domain allowlist (leave empty for unrestricted research).\n\n### Demo prompts\n- *Research what products firecrawl.dev offers and compare with crawl4ai.com \u2014 give me a pricing + features table.*\n- *Map docs.n8n.io, then batch-scrape the 10 pages most relevant to AI agents and summarize.*\n- *Extract the job title and salary range from jobs.lever.co/firecrawl using a schema.*\n- *How many credits have we spent this session?*\n- *Scrape stripe.com/pricing and give me their Standard plan details with citations.*",
        "height": 720,
        "width": 800,
        "color": 6
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -700,
        -780
      ],
      "id": "sticky-fc-main-header",
      "name": "README"
    },
    {
      "parameters": {
        "content": "### 1. Chat Trigger\nUser-facing entry point. `mode: hostedChat` exposes a public chat URL you can share or embed. `sessionId` (auto-generated per conversation) is what isolates memory + credit tracking per user.\n\nFor private use: set `public: false` and embed via the `@n8n/chat` widget with a JWT in `metadata`.\n\nFor programmatic use, see the companion `02-webhook-firecrawl-agent` workflow.",
        "height": 280,
        "width": 320,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -700,
        -20
      ],
      "id": "sticky-fc-trigger",
      "name": "Trigger note"
    },
    {
      "parameters": {
        "content": "### 2. The Agent\nLangChain **Tools Agent** (maxIterations=12). The system message encodes the winning patterns:\n- **Map-first** for site research\n- **Cache-aware** (trust 24h cache_hit)\n- **Credit-discipline** (check_credits every 5 calls)\n- **Citation-first output** (inline `[n]` + Sources table)\n- **Tool decision table** (exact tool per task type)",
        "height": 280,
        "width": 320,
        "color": 3
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -360,
        -40
      ],
      "id": "sticky-fc-agent",
      "name": "Agent note"
    },
    {
      "parameters": {
        "content": "### 3. Brain (LLM + Memory)\n**OpenRouter \u2192 Claude Sonnet 4.6** at temperature 0.2. Swap the `model` field for any OpenRouter-supported model.\n\n**Postgres Chat Memory:** conversation history in `public.n8n_chat_histories`, keyed by the chat sessionId. Last 20 messages persist per turn.",
        "height": 240,
        "width": 320,
        "color": 3
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -520,
        520
      ],
      "id": "sticky-fc-brain",
      "name": "Brain note"
    },
    {
      "parameters": {
        "content": "### 4. Discovery tools (read-lite)\n**search_web** \u2014 Firecrawl web search, 2 credits per 10 results.\n**map_site** \u2014 URL inventory of a site, 1 credit flat.\n**check_credits** \u2014 pure Postgres read, 0 credits.\n\nUse these BEFORE expensive operations. The winning pattern: `map_site` \u2192 filter via `think` \u2192 `batch_scrape`.",
        "height": 280,
        "width": 400,
        "color": 5
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -180,
        520
      ],
      "id": "sticky-fc-discovery",
      "name": "Discovery tools"
    },
    {
      "parameters": {
        "content": "### 5. Scrape tools (page content)\n**scrape_url** \u2014 single URL, 1 credit, 24h cached.\n**batch_scrape** \u2014 up to 25 URLs in parallel, 1 credit each.\n**crawl_site** \u2014 bounded recursive crawl (max 50 pages, depth 3).\n**extract_data** \u2014 LLM structured extraction, 5 credits per URL.",
        "height": 280,
        "width": 400,
        "color": 5
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        260,
        520
      ],
      "id": "sticky-fc-scrape",
      "name": "Scrape tools"
    },
    {
      "parameters": {
        "content": "### 6. Autopilot + Sandbox tools\n**firecrawl_autopilot** \u2014 hands off to Firecrawl's own server-side agent for multi-page tasks with unknown structure.\n**browser_session** \u2014 persistent browser (create/execute/delete). Required for login-gated or heavy-JS sites.\n**think** \u2014 scratchpad. Use before any expensive call and after any tool error.",
        "height": 280,
        "width": 400,
        "color": 5
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        700,
        520
      ],
      "id": "sticky-fc-autopilot",
      "name": "Autopilot tools"
    },
    {
      "parameters": {
        "public": true,
        "mode": "hostedChat",
        "initialMessages": "Hi! I'm your Firecrawl web research agent.\nI can search, scrape, map, crawl, and extract structured data from the open web \u2014 then give you citation-backed answers.\n\nTry: *Research what Firecrawl offers and compare with Apify \u2014 give me a features table with citations.*",
        "options": {
          "title": "Firecrawl Web Research Agent",
          "subtitle": "Ask. I research. You get citations.",
          "responseMode": "lastNode"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "typeVersion": 1.1,
      "position": [
        -680,
        0
      ],
      "id": "fc-chat-trigger",
      "name": "When chat message received"
    },
    {
      "parameters": {
        "promptType": "auto",
        "options": {
          "maxIterations": 12,
          "systemMessage": "You are a senior web research analyst powered by Firecrawl. Your job is to turn user questions into well-sourced, citation-dense research briefs \u2014 not just raw scraped text.\n\n## Your tools and when to call them\n\n**Discovery (cheap, call liberally):**\n- `search_web(query, limit?, scrape_results?)` \u2014 Firecrawl web search. 2 credits per 10 results. Use when you don't know which URLs matter.\n- `map_site(url, search?, limit?)` \u2014 returns every URL on a site. 1 credit flat. Use this BEFORE crawling or batch-scraping any site.\n- `check_credits(session_id?, credit_cap?)` \u2014 pure Postgres read, 0 credits. Call this every 5 tool calls and BEFORE any crawl/batch/extract call.\n\n**Scrape (page content \u2014 costs credits):**\n- `scrape_url(url, formats?, only_main_content?, include_screenshot?, force_refresh?)` \u2014 single URL, 1 credit, 24h cached. `cache_hit: true` means 0 credits charged. Trust the cache unless the user asks for fresh data.\n- `batch_scrape(urls[], formats?)` \u2014 up to 25 URLs in parallel, 1 credit each. Prefer this over looping scrape_url.\n- `crawl_site(url, max_pages?, max_depth?, include_patterns?, exclude_patterns?)` \u2014 bounded recursive crawl. Hard caps: 50 pages, depth 3. Last resort when you MUST traverse the whole site.\n- `extract_data(urls[], prompt?, schema?)` \u2014 LLM-powered structured extraction. 5 credits per URL. Use only when you need schema-conforming JSON, not free-form text.\n\n**Advanced:**\n- `firecrawl_autopilot(prompt, url?, max_wait?)` \u2014 hand the goal to Firecrawl's own server-side agent. Best for multi-page tasks where you don't know the site structure. Variable cost.\n- `browser_session(action, session_id?, code?, language?, url?)` \u2014 persistent browser for login-gated or heavy-JS sites. Actions: `create`, `execute`, `delete`, `list`. ALWAYS call `delete` when done \u2014 stale sessions hold resources.\n- `think(thought)` \u2014 your scratchpad. Call before complex plans, to decompose multi-step tasks, and ALWAYS after a tool error before retrying.\n\n## THE WINNING PATTERNS \u2014 follow these in order\n\n### Pattern 1: Map-first research on a site\nIf the user asks about a *site* (vs a specific URL), DO NOT crawl blind. Do this:\n1. `map_site(url)` \u2192 get every URL (1 credit)\n2. `think` \u2192 filter the URL list to the 5-15 most relevant ones\n3. `batch_scrape(filtered_urls)` \u2192 scrape only what matters\n\nThis pattern routinely saves 30-80% of credits vs `crawl_site`. Use it whenever you can.\n\n### Pattern 2: Use the cache\n- If you scraped a URL earlier in this conversation, trust it. Don't re-scrape.\n- When `scrape_url` or `batch_scrape` returns `cache_hit: true`, that means 0 credits were charged. Note this in your observations.\n- Pass `force_refresh: true` ONLY when the user explicitly asks for fresh data or you know the page changed.\n\n### Pattern 3: Credit discipline\n- Call `check_credits` every 5 tool calls and BEFORE any `crawl_site`, `batch_scrape`, `extract_data`, or `firecrawl_autopilot`.\n- If `over_budget: true` or `near_cap: true`, STOP calling new tools. Summarize with what you have and tell the user your budget is exhausted.\n- Include a credit-usage line in your final answer's observations (e.g., `Used 12 Firecrawl credits this turn.`).\n\n### Pattern 4: Citation-first output\nEvery factual claim must carry an inline `[n]` citation. Sources map to the numbered list in the Sources section. No citation = no claim. This is non-negotiable.\n\n### Pattern 5: Tool selection decision table\n| User intent | Right tool |\n|---|---|\n| \"What does X say?\" (single URL) | `scrape_url` |\n| \"Find pages about X\" | `search_web` |\n| \"What's on site Y?\" | `map_site` \u2192 `batch_scrape` on selected URLs |\n| \"Scrape these 10 URLs\" | `batch_scrape` |\n| \"Exhaustive crawl of a section\" | `crawl_site` with tight `include_patterns` |\n| \"Extract prices / job titles / spec fields\" | `extract_data` with a schema |\n| \"Navigate a heavy-JS or logged-in site\" | `browser_session` |\n| \"Autonomous deep research, you figure it out\" | `firecrawl_autopilot` |\n| \"How much have I spent?\" | `check_credits` |\n\n## Hard rules\n- **No writes, no destructive ops.** You only read the web and read/write the credit ledger + cache.\n- **Respect allowlist.** If `firecrawl_allowed_domains` is populated, the sub-workflows block disallowed URLs. Don't try to work around it \u2014 ask the user to add the domain.\n- **Respect robots.txt signals** in Firecrawl responses. If Firecrawl returns an error indicating a page is blocked, report the restriction to the user; don't try alternative extractions without explicit confirmation.\n- **Never invent URLs.** Only cite URLs you actually scraped or that came back from `search_web` / `map_site`.\n- **Never invent facts.** Everything in your answer must trace back to a scraped page. If you can't source it, say so.\n- **Cap enforcement is in the sub-workflows.** You don't have to police `max_pages` or `limit` \u2014 the sub-workflows clamp inputs. But prefer tighter limits than the caps to save credits.\n- **On tool error:** call `think` \u2192 diagnose (wrong URL, timeout, schema mismatch) \u2192 retry ONCE with a fix. After 2 failures, ask the user.\n- **maxIterations is 12.** Budget your tool calls. If you're at 10 calls and haven't got enough, stop and answer with what you have.\n\n## Output format \u2014 STRICT\nEvery response MUST have these 5 sections in order:\n\n### 1. Headline\nONE bold sentence with the key finding + the most important numbers. Example:\n`**Firecrawl charges $0 for 500 credits/mo on Hobby and $19/mo for 10K credits on Standard [1], while Apify starts at $49/mo for a $49 credit pool [2].**`\n\n### 2. Summary\n2-4 short paragraphs answering the user's question. Every factual claim carries an inline `[n]`. Numbers, product names, quotes, and dates all need citations. Use the format:\n- Claim \u2192 `[1]`\n- Multi-source claim \u2192 `[1][3]`\n- Quoted text \u2192 `\"exact quote\" [2]`\n\n### 3. Evidence table\nA markdown pipe table with three columns: `Finding`, `Source`, `Confidence` (high/medium/low based on whether the source states it directly vs implies it).\n\n```\n| Finding                           | Source | Confidence |\n| --------------------------------- | ------ | ---------- |\n| Firecrawl Hobby tier is free      | [1]    | high       |\n| Apify starts at $49/mo            | [2]    | high       |\n| Firecrawl added sandbox in 2026   | [3]    | medium     |\n```\n\n### 4. Sources\nNumbered list, one line per source:\n```\n[1] Firecrawl Pricing \u2014 https://firecrawl.dev/pricing (scraped 2026-04-23)\n[2] Apify Pricing \u2014 https://apify.com/pricing (scraped 2026-04-23)\n```\n- Use the actual scraped URL and the actual `scraped_at` timestamp from the tool response.\n- The numbering in the text, table, and this list must line up exactly.\n\n### 5. Observations\n2-4 bullets of NON-OBVIOUS insights:\n- Trade-offs not explicit on either page\n- Gaps in the source material (what wasn't stated)\n- Credit usage note: `Used N Firecrawl credits this turn (M cache hits).`\n- A concrete follow-up the user could ask\n\nSeparate each section with a blank line. No greetings, no \"Here is your analysis\", no closing pleasantries. Keep the whole response tight."
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 1.9,
      "position": [
        -340,
        0
      ],
      "id": "fc-agent-main",
      "name": "Firecrawl Web Agent"
    },
    {
      "parameters": {
        "model": "anthropic/claude-sonnet-4.6",
        "options": {
          "temperature": 0.2
        }
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        -500,
        260
      ],
      "id": "fc-llm",
      "name": "OpenRouter Chat Model"
    },
    {
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "={{ $('When chat message received').item.json.sessionId }}",
        "contextWindowLength": 20,
        "tableName": "n8n_chat_histories"
      },
      "type": "@n8n/n8n-nodes-langchain.memoryPostgresChat",
      "typeVersion": 1.3,
      "position": [
        -340,
        260
      ],
      "id": "fc-memory",
      "name": "Postgres Chat Memory"
    },
    {
      "parameters": {
        "name": "search_web",
        "description": "Searches the open web via Firecrawl. Returns up to `limit` ranked results with title, URL, description, and optionally scraped markdown. Costs 2 credits per 10 results (plus 1 per result if scrape_results=true). Inputs: query (required), limit (default 5, max 20), scrape_results (default false).",
        "workflowId": {
          "__rl": true,
          "value": "Fc04SubSearch01x",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "query": "={{ $fromAI('query', 'The search query string.', 'string') }}",
            "limit": "={{ $fromAI('limit', 'How many results to return (1-20).', 'number') }}",
            "scrape_results": "={{ $fromAI('scrape_results', 'If true, scrape each result and include markdown (adds 1 credit per result).', 'boolean') }}",
            "session_id": "={{ $('When chat message received').item.json.sessionId }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "query",
              "displayName": "query",
              "required": true,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "limit",
              "displayName": "limit",
              "required": false,
              "type": "number",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "scrape_results",
              "displayName": "scrape_results",
              "required": false,
              "type": "boolean",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "session_id",
              "displayName": "session_id",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.1,
      "position": [
        -140,
        260
      ],
      "id": "fc-tool-search",
      "name": "search_web"
    },
    {
      "parameters": {
        "name": "map_site",
        "description": "Returns every discoverable URL on a site \u2014 the fastest way to scope research. 1 credit flat regardless of URL count. Use this BEFORE crawl_site or batch_scrape so you know what pages exist. Inputs: url (site root), search (optional keyword filter), limit (default 500, max 5000).",
        "workflowId": {
          "__rl": true,
          "value": "Fc05SubMapSite01",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "url": "={{ $fromAI('url', 'The site root URL to map, e.g. https://firecrawl.dev', 'string') }}",
            "search": "={{ $fromAI('search', 'Optional keyword filter. Only URLs whose path contains this string are returned.', 'string') }}",
            "limit": "={{ $fromAI('limit', 'Max number of URLs to return (default 500).', 'number') }}",
            "session_id": "={{ $('When chat message received').item.json.sessionId }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "url",
              "displayName": "url",
              "required": true,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "search",
              "displayName": "search",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "limit",
              "displayName": "limit",
              "required": false,
              "type": "number",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "session_id",
              "displayName": "session_id",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.1,
      "position": [
        40,
        260
      ],
      "id": "fc-tool-map",
      "name": "map_site"
    },
    {
      "parameters": {
        "name": "scrape_url",
        "description": "Scrapes a single URL and returns clean markdown + title + links. 1 credit per call. Results are cached in Postgres for 24h \u2014 identical URLs within that window return cache_hit=true for 0 credits. Inputs: url (required), formats (default ['markdown','links']), only_main_content (default true), include_screenshot (default false), force_refresh (default false \u2014 set true to bypass cache).",
        "workflowId": {
          "__rl": true,
          "value": "Fc03SubScrapeC01",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "url": "={{ $fromAI('url', 'The single URL to scrape.', 'string') }}",
            "formats": "={{ $fromAI('formats', 'Array of output formats: markdown, html, links, screenshot. Default [\"markdown\",\"links\"].', 'json') }}",
            "only_main_content": "={{ $fromAI('only_main_content', 'Strip navigation/footer/ads. Default true.', 'boolean') }}",
            "include_screenshot": "={{ $fromAI('include_screenshot', 'Also return a PNG screenshot URL. Default false.', 'boolean') }}",
            "force_refresh": "={{ $fromAI('force_refresh', 'Skip the 24h cache and re-fetch. Default false.', 'boolean') }}",
            "session_id": "={{ $('When chat message received').item.json.sessionId }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "url",
              "displayName": "url",
              "required": true,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "formats",
              "displayName": "formats",
              "required": false,
              "type": "array",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "only_main_content",
              "displayName": "only_main_content",
              "required": false,
              "type": "boolean",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "include_screenshot",
              "displayName": "include_screenshot",
              "required": false,
              "type": "boolean",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "force_refresh",
              "displayName": "force_refresh",
              "required": false,
              "type": "boolean",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "session_id",
              "displayName": "session_id",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.1,
      "position": [
        220,
        260
      ],
      "id": "fc-tool-scrape",
      "name": "scrape_url"
    },
    {
      "parameters": {
        "name": "batch_scrape",
        "description": "Scrapes up to 25 URLs in parallel via Firecrawl. More efficient than looping scrape_url. 1 credit per successful URL. Hard cap: 25 URLs per call. Inputs: urls (required array), formats (default ['markdown']), only_main_content (default true).",
        "workflowId": {
          "__rl": true,
          "value": "Fc07SubBatchSc01",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "urls": "={{ $fromAI('urls', 'Array of URLs to scrape, max 25 per call.', 'json') }}",
            "formats": "={{ $fromAI('formats', 'Array of formats, default [\"markdown\"].', 'json') }}",
            "only_main_content": "={{ $fromAI('only_main_content', 'Strip navigation/footer/ads. Default true.', 'boolean') }}",
            "session_id": "={{ $('When chat message received').item.json.sessionId }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "urls",
              "displayName": "urls",
              "required": true,
              "type": "array",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "formats",
              "displayName": "formats",
              "required": false,
              "type": "array",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "only_main_content",
              "displayName": "only_main_content",
              "required": false,
              "type": "boolean",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "session_id",
              "displayName": "session_id",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.1,
      "position": [
        400,
        260
      ],
      "id": "fc-tool-batch",
      "name": "batch_scrape"
    },
    {
      "parameters": {
        "name": "crawl_site",
        "description": "Recursively crawls a site from a starting URL. Hard caps: 50 pages, depth 3. 1 credit per page scraped. Prefer map_site + batch_scrape unless the user specifically wants exhaustive traversal. Inputs: url (root), max_pages (default 25, max 50), max_depth (default 2, max 3), include_patterns (glob array), exclude_patterns (glob array).",
        "workflowId": {
          "__rl": true,
          "value": "Fc06SubCrawlB01x",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "url": "={{ $fromAI('url', 'The site root or starting URL to crawl.', 'string') }}",
            "max_pages": "={{ $fromAI('max_pages', 'Max pages to scrape (default 25, hard cap 50).', 'number') }}",
            "max_depth": "={{ $fromAI('max_depth', 'Max link-depth to follow (default 2, hard cap 3).', 'number') }}",
            "include_patterns": "={{ $fromAI('include_patterns', 'URL glob patterns to include, e.g. [\"/docs/*\"].', 'json') }}",
            "exclude_patterns": "={{ $fromAI('exclude_patterns', 'URL glob patterns to exclude.', 'json') }}",
            "session_id": "={{ $('When chat message received').item.json.sessionId }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "url",
              "displayName": "url",
              "required": true,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "max_pages",
              "displayName": "max_pages",
              "required": false,
              "type": "number",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "max_depth",
              "displayName": "max_depth",
              "required": false,
              "type": "number",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "include_patterns",
              "displayName": "include_patterns",
              "required": false,
              "type": "array",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "exclude_patterns",
              "displayName": "exclude_patterns",
              "required": false,
              "type": "array",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "session_id",
              "displayName": "session_id",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.1,
      "position": [
        580,
        260
      ],
      "id": "fc-tool-crawl",
      "name": "crawl_site"
    },
    {
      "parameters": {
        "name": "extract_data",
        "description": "LLM-powered structured extraction. Returns JSON conforming to a JSON Schema (validated on return). 5 credits per URL. Hard cap: 10 URLs per call. Use only when you need structured fields, not free-form text. Inputs: urls (required), prompt (describe what to extract) OR schema (JSON Schema object).",
        "workflowId": {
          "__rl": true,
          "value": "Fc08SubExtract01",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "urls": "={{ $fromAI('urls', 'Array of URLs to extract from, max 10 per call.', 'json') }}",
            "prompt": "={{ $fromAI('prompt', 'Natural-language description of what to extract.', 'string') }}",
            "schema": "={{ $fromAI('schema', 'Optional JSON Schema object for the output structure.', 'json') }}",
            "session_id": "={{ $('When chat message received').item.json.sessionId }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "urls",
              "displayName": "urls",
              "required": true,
              "type": "array",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "prompt",
              "displayName": "prompt",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "schema",
              "displayName": "schema",
              "required": false,
              "type": "object",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "session_id",
              "displayName": "session_id",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.1,
      "position": [
        760,
        260
      ],
      "id": "fc-tool-extract",
      "name": "extract_data"
    },
    {
      "parameters": {
        "name": "firecrawl_autopilot",
        "description": "Delegates a complete research goal to Firecrawl's own server-side agent. Returns whatever it extracts. Use for multi-page tasks where you don't know the site structure upfront. Variable credit cost (typically 5-30). Inputs: prompt (required goal), url (optional starting point), max_wait (seconds to wait, default 180, max 300).",
        "workflowId": {
          "__rl": true,
          "value": "Fc09SubFcAgent01",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "prompt": "={{ $fromAI('prompt', 'Natural-language research goal for the Firecrawl autopilot.', 'string') }}",
            "url": "={{ $fromAI('url', 'Optional starting URL hint.', 'string') }}",
            "max_wait": "={{ $fromAI('max_wait', 'Max seconds to wait for sync completion (default 180, max 300).', 'number') }}",
            "session_id": "={{ $('When chat message received').item.json.sessionId }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "prompt",
              "displayName": "prompt",
              "required": true,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "url",
              "displayName": "url",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "max_wait",
              "displayName": "max_wait",
              "required": false,
              "type": "number",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "session_id",
              "displayName": "session_id",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.1,
      "position": [
        940,
        260
      ],
      "id": "fc-tool-autopilot",
      "name": "firecrawl_autopilot"
    },
    {
      "parameters": {
        "name": "browser_session",
        "description": "Manages a persistent browser sandbox session for login-gated or heavy-JS sites. Actions: `create` (opens a session, returns session_id), `execute` (runs JS/python/bash in the session), `delete` (closes \u2014 ALWAYS call when done), `list` (lists active sessions). Inputs: action (required), session_id (for execute/delete), code (for execute), language (default javascript), url (for create).",
        "workflowId": {
          "__rl": true,
          "value": "Fc10SubSandbox01",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "action": "={{ $fromAI('action', 'One of: create, execute, delete, list.', 'string') }}",
            "session_id": "={{ $fromAI('session_id', 'Sandbox session id (required for execute and delete).', 'string') }}",
            "code": "={{ $fromAI('code', 'Code to execute in the browser sandbox.', 'string') }}",
            "language": "={{ $fromAI('language', 'Language: javascript (default), python, bash.', 'string') }}",
            "url": "={{ $fromAI('url', 'Optional starting URL for create action.', 'string') }}",
            "session_id_caller": "={{ $('When chat message received').item.json.sessionId }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "action",
              "displayName": "action",
              "required": true,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "session_id",
              "displayName": "session_id",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "code",
              "displayName": "code",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "language",
              "displayName": "language",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "url",
              "displayName": "url",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "session_id_caller",
              "displayName": "session_id_caller",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.1,
      "position": [
        1120,
        260
      ],
      "id": "fc-tool-sandbox",
      "name": "browser_session"
    },
    {
      "parameters": {
        "name": "check_credits",
        "description": "Returns how many Firecrawl credits this session has spent, grouped by operation, plus recent call history and an advice string. Pure Postgres read \u2014 0 credits. Call every 5 tool calls and BEFORE any crawl/batch/extract. Inputs: session_id (optional, defaults to current), credit_cap (optional, default 500).",
        "workflowId": {
          "__rl": true,
          "value": "Fc11SubCredits01",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "session_id": "={{ $('When chat message received').item.json.sessionId }}",
            "credit_cap": "={{ $fromAI('credit_cap', 'Soft session budget in credits (default 500).', 'number') }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "session_id",
              "displayName": "session_id",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "credit_cap",
              "displayName": "credit_cap",
              "required": false,
              "type": "number",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.1,
      "position": [
        1300,
        260
      ],
      "id": "fc-tool-credits",
      "name": "check_credits"
    },
    {
      "parameters": {
        "name": "think",
        "description": "Internal scratchpad. Use to plan a multi-step research path, to decompose a vague question, or to diagnose a tool error before retrying. Input: `thought` (string). Returns the thought back with a timestamp. Use this generously BEFORE expensive tool calls (crawl/batch/extract) and ALWAYS after an error.",
        "language": "javaScript",
        "jsCode": "const t = typeof query === 'string' ? query : (query && query.thought) ? query.thought : JSON.stringify(query);\nreturn `Thought recorded at ${new Date().toISOString()}: ${t}`;",
        "specifyInputSchema": true,
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"thought\": { \"type\": \"string\", \"description\": \"What you're planning, filtering, or diagnosing.\" }\n  },\n  \"required\": [\"thought\"]\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.toolCode",
      "typeVersion": 1.3,
      "position": [
        1480,
        260
      ],
      "id": "fc-tool-think",
      "name": "think"
    }
  ],
  "connections": {
    "When chat message received": {
      "main": [
        [
          {
            "node": "Firecrawl Web Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Firecrawl Web Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Postgres Chat Memory": {
      "ai_memory": [
        [
          {
            "node": "Firecrawl Web Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "search_web": {
      "ai_tool": [
        [
          {
            "node": "Firecrawl Web Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "map_site": {
      "ai_tool": [
        [
          {
            "node": "Firecrawl Web Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "scrape_url": {
      "ai_tool": [
        [
          {
            "node": "Firecrawl Web Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "batch_scrape": {
      "ai_tool": [
        [
          {
            "node": "Firecrawl Web Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "crawl_site": {
      "ai_tool": [
        [
          {
            "node": "Firecrawl Web Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "extract_data": {
      "ai_tool": [
        [
          {
            "node": "Firecrawl Web Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "firecrawl_autopilot": {
      "ai_tool": [
        [
          {
            "node": "Firecrawl Web Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "browser_session": {
      "ai_tool": [
        [
          {
            "node": "Firecrawl Web Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "check_credits": {
      "ai_tool": [
        [
          {
            "node": "Firecrawl Web Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "think": {
      "ai_tool": [
        [
          {
            "node": "Firecrawl Web Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    }
  }
}