{
  "name": "[FC Sub] Search the Web",
  "settings": {
    "executionOrder": "v1"
  },
  "nodes": [
    {
      "parameters": {
        "content": "## [FC Sub] Search the Web\n**Purpose:** Runs a Firecrawl `/v1/search` query. Returns ranked web results with titles, URLs, and descriptions \u2014 optionally scraping each result.\n\n**Called by:** main agent's `search_web` tool.\n\n**Inputs:** `query` (required), `limit?` (default 5, max 20), `scrape_results?` (default false), `session_id?`.\n\n**Credit cost:** 2 credits per 10 results (per Firecrawl pricing). If `scrape_results: true`, add 1 credit per result scraped.\n\n**Flow:**\n1. **Trigger** \u2014 receives the search request.\n2. **Prep** \u2014 clamps limit to 20, normalizes params.\n3. **Call Firecrawl Search** \u2014 POST `/v1/search`.\n4. **Shape** \u2014 extract `results[]` with title/url/description/markdown.\n5. **Log Ledger** \u2014 writes one row with computed credit cost.\n6. **Return** \u2014 `{ query, results[], result_count, credits_used }`.\n\n**Credentials:** Firecrawl API, Postgres RW.",
        "height": 520,
        "width": 580,
        "color": 6
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -560
      ],
      "id": "sticky-fc-search",
      "name": "README"
    },
    {
      "parameters": {
        "inputSource": "passthrough"
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        0
      ],
      "id": "fc-search-trigger",
      "name": "When Executed by Another Workflow"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst q = input.query_obj || input.query || {};\n// `query` is both the natural param name AND a common n8n wrapper.\n// Extract carefully: if the input has a top-level string `query`, that's the search query.\nlet query;\nif (typeof input.query === 'string') query = input.query;\nelse if (q && typeof q === 'object' && typeof q.query === 'string') query = q.query;\nelse if (typeof input.q === 'string') query = input.q;\nif (!query || !query.trim()) throw new Error('search_web requires a `query` string.');\n\nconst pick = (k, def) => {\n  if (input[k] !== undefined) return input[k];\n  if (q && typeof q === 'object' && q[k] !== undefined) return q[k];\n  return def;\n};\n\nlet limit = Number(pick('limit', 5));\nif (!Number.isFinite(limit) || limit < 1) limit = 5;\nif (limit > 20) limit = 20; // hard cap \u2014 credit protection\n\nconst scrape_results = pick('scrape_results', false) === true;\nconst session_id = (pick('session_id') || $execution.id || 'no-session').toString();\n\nreturn [{ json: {\n  query: query.trim(),\n  limit,\n  scrape_results,\n  session_id,\n  execution_id: $execution.id\n} }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "id": "fc-search-prep",
      "name": "Prep Input"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.firecrawl.dev/v1/search",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"query\": {{ JSON.stringify($json.query) }},\n  \"limit\": {{ $json.limit }}\n  {{ $json.scrape_results ? ', \"scrapeOptions\": { \"formats\": [\"markdown\"] }' : '' }}\n}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 120000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        0
      ],
      "id": "fc-search-http",
      "name": "Call Firecrawl Search"
    },
    {
      "parameters": {
        "jsCode": "const prep = $('Prep Input').first().json;\nconst resp = $input.first().json;\nconst data = Array.isArray(resp.data) ? resp.data : (resp.results || []);\nconst MAX_MD = 8000; // per-result markdown cap when scrape_results is true\n\nconst results = data.map(r => ({\n  title: r.title || r.metadata?.title || '',\n  url: r.url || '',\n  description: r.description || r.metadata?.description || '',\n  markdown: prep.scrape_results && r.markdown\n    ? (r.markdown.length > MAX_MD ? r.markdown.slice(0, MAX_MD) + '\\n\\n[...truncated]' : r.markdown)\n    : undefined\n}));\n\nconst result_count = results.length;\n// Search pricing: 2 credits per 10 results. Scrape add-on: 1 per result.\nconst search_credits = Math.ceil(result_count / 10) * 2;\nconst scrape_credits = prep.scrape_results ? result_count : 0;\nconst credits_used = search_credits + scrape_credits;\n\nreturn [{ json: {\n  query: prep.query,\n  results,\n  result_count,\n  credits_used,\n  session_id: prep.session_id,\n  execution_id: prep.execution_id\n} }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        0
      ],
      "id": "fc-search-shape",
      "name": "Shape Response"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO public.firecrawl_credit_ledger (session_id, execution_id, operation, credits_used, status, metadata)\nVALUES ($1, $2, 'search', $3, 'ok', $4::jsonb);",
        "options": {
          "queryReplacement": "={{ $json.session_id }}, {{ $json.execution_id }}, {{ $json.credits_used }}, {{ JSON.stringify({ query: $json.query, result_count: $json.result_count }) }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        880,
        0
      ],
      "id": "fc-search-ledger",
      "name": "Log Credit Ledger"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s1",
              "name": "query",
              "value": "={{ $('Shape Response').first().json.query }}",
              "type": "string"
            },
            {
              "id": "s2",
              "name": "results",
              "value": "={{ $('Shape Response').first().json.results }}",
              "type": "array"
            },
            {
              "id": "s3",
              "name": "result_count",
              "value": "={{ $('Shape Response').first().json.result_count }}",
              "type": "number"
            },
            {
              "id": "s4",
              "name": "credits_used",
              "value": "={{ $('Shape Response').first().json.credits_used }}",
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1100,
        0
      ],
      "id": "fc-search-return",
      "name": "Return Results"
    }
  ],
  "connections": {
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "Prep Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prep Input": {
      "main": [
        [
          {
            "node": "Call Firecrawl Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Firecrawl Search": {
      "main": [
        [
          {
            "node": "Shape Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Shape Response": {
      "main": [
        [
          {
            "node": "Log Credit Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Credit Ledger": {
      "main": [
        [
          {
            "node": "Return Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}