{
  "name": "Redirect",
  "nodes": [
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "5945c51f-f560-45f9-b99e-a15b43266b21",
              "name": "url",
              "value": "={{ $json[\"Target URL\"] }}",
              "type": "string"
            },
            {
              "id": "2c59aae9-3112-4a87-ada9-1bb4ef7520bb",
              "name": "change_request",
              "value": "={{ $json[\"Change Request\"] }}",
              "type": "string"
            },
            {
              "id": "f9364dd9-0d6b-482e-9a31-490b1bfc05ce",
              "name": "sitemap",
              "value": "={{ $json.Sitemap }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -240,
        144
      ],
      "id": "8db5ffd5-68c0-4e63-b8c1-09454f8ec768",
      "name": "Edit Fields"
    },
    {
      "parameters": {
        "model": "openai/gpt-5.4-nano",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        1296,
        368
      ],
      "id": "de8cab82-7567-4a6c-9845-d84d2c1281c4",
      "name": "OpenRouter Chat Model",
      "credentials": {
        "openRouterApi": {
          "id": "baaHEuVk7u6Z0Liu",
          "name": "OpenRouter account"
        }
      }
    },
    {
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "1231242"
      },
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "typeVersion": 1.3,
      "position": [
        1392,
        384
      ],
      "id": "18277ece-cffb-4678-8f70-11085eb54d06",
      "name": "Simple Memory"
    },
    {
      "parameters": {
        "jsonSchemaExample": "{\"output\":{\n\t\"match\": \"/assets/v1/\",\n\t\"replace\": \"/assets/v2/\"\n}}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        1488,
        384
      ],
      "id": "93464c22-9755-4e87-86ea-d13a31aa842b",
      "name": "Structured Output Parser"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $('Merge').first()?.json?.failures ? \"Previous attempt failed. Fix these issues:\\n\" + JSON.stringify($('Merge').first().json.failures, null, 2) : \"\" }}\n\n\n# Regex Match/Replace Generator for URL Rewrite Rules\n\nYou are a regex match/replace generator for URL rewrite rules. Given a URL \nand a plain-English change request, output a single JSON object:\n\n{\"match\": \"REGEX_PATTERN\", \"replace\": \"REPLACEMENT_STRING\"}\n\n## Mandatory rule: every match ends with this exact capture suffix\n\nEvery match targeting the end of a path (redirects, full path swaps) MUST \nend with the following four capture groups, in this order:\n\n  (\\/?)(\\/)?(\\?.*)?(#.*)?\n\n- Group 1: optional trailing slash\n- Group 2: optional secondary slash (kept for safety/consistency — will \n  typically be empty since group 1 already consumes a slash if present)\n- Group 3: optional query string\n- Group 4: optional hash fragment\n\nEvery replace MUST reinstate all four groups, in order, immediately after \nthe new path: `$1$2$3$4`. This is required on every single output, \nregardless of whether the user's request mentions a trailing slash, \nparameters, or a hash. The user will never ask for this explicitly — it is \nnot optional and must never be omitted.\n\nNo negative lookeaheads! (\\/!?) will be rejected. Use this instead for optional trailing slash: (\\/?)\n\nThis exact suffix must work identically against a URL with or without a \ntrailing slash before the query string — e.g. both `/apps?test=param2` and \n`/apps/?test=param2` must match the same rule and produce correctly \nreinstated output.\n\n## Standard template — full path or end-of-URL changes\n\n  Match:   \"\\/oldpath(\\/?)(\\/)?(\\?.*)?(#.*)?$\"\n  Replace: \"/newpath$1$2$3$4\"\n\n## Standard template — mid-path segment/slug changes (more path follows)\n\nWhen the changed segment is followed by more path (not the end of the \nURL), capture the remaining path as its own group, then still apply the \nmandatory suffix for query/hash:\n\n  Match:   \"\\/oldsegment(\\/[^?#]*)?(\\?.*)?(#.*)?$\"\n  Replace: \"/newsegment$1$2$3\"\n\n## Parameter value changes — capture dynamically, reinstate the rest\n\n  Match:   \"([?&])paramname=([^&#]*)\"\n  Replace: \"$1paramname=$2-suffix\"\n\n## Removing a parameter — reinstate the remaining parameter list\n\n  [\n    {\"match\": \"([?&])paramname=[^&#]*&\", \"replace\": \"$1\"},\n    {\"match\": \"[?&]paramname=[^&#]*$\", \"replace\": \"\"}\n  ]\n\n## Output format rules\n\n- Output ONLY the JSON object — no prose, no markdown \n  fences, no explanation, unless explicitly asked to explain.\n- Escape forward slashes and special regex characters appropriately for \n  the target engine (assume JavaScript regex syntax unless told otherwise).\n- Never invent parameter values that weren't in the request — only \n  hardcode a value if the user's change request explicitly names it.\n- Every replace string must account for every capture group in its match.\n- The `(\\/?)(\\/)?(\\?.*)?(#.*)?` suffix (or `$1$2$3$4` in the replace) is \n  required on every end-of-path rule, with no exceptions.\n- No negative lookaheads\n- No escape on the replace\n\n## Examples\n\nInput:\n  URL: https://iqbala.com/mypage\n  Change Request: redirect to /mypage2\nOutput:\n  {\"match\": \"(https:\\/\\/iqbala.com\\/)mypage(\\/?)(\\/)?(\\?.*)?(#.*)?$\", \"replace\": \"$1mypage2$2$3$4$5\"}\n\n\n(Both inputs above resolve to the identical rule — this pattern handles \nboth URL shapes without needing separate rules.)\n\nInput:\n  URL: https://mysite.com/checkout\n  Change Request: redirect to /checkout-v2\nOutput:\n  {\"match\": \"(https:\\/\\/mysite.com\\/)checkout(\\/?)(\\/)?(\\?.*)?(#.*)?$\", \"replace\": \"$checkout-v2$2$3$4$5\"}\n\nInput:\n  URL: https://mysite.com/products/widget/reviews\n  Change Request: rename widget to gadget\nOutput:\n  {\"match\": \"(https:\\/\\/mysite\\.com\\/products\\/)widget(\\/reviews)(\\/?)(\\/)?(\\?.*)?(#.*)?$\", \"replace\": \"$1gadgets$2$3$4$5\"}\n\nInput:\n  URL: https://mysite.com/?ref=affiliate123\n  Change Request: append \"-tracked\" to whatever the ref value currently is\nOutput:\n  {\"match\": \"(https:\\/\\/mysite\\.com\\/)\\?ref=affiliate123(&[^#]*)?(#.*)?$\", \"replace\": \"$1?ref=affiliate123-tracked$2$3\"}\n\nInput:\n  URL: https://blog.example.com/posts/summer-sale-2024/?utm_campaign=july\n  Change Request: replace slug summer-sale-2024 with summer-sale-2025\nOutput:\n  {\"match\": \"(https:\\/\\/blog\\.example\\.com\\/posts\\/)summer-sale-2024(\\/?)(\\/)?(\\?.*)?(#.*)?$\", \"replace\": \"$1summer-sale-2025$2$3$4$5\"}\n\n%INPUT%\nURL: {{ $('Edit Fields').item.json.url }}\nSpecification document: \n{{ $json.text }}\n%OUTPUT%\n\n",
        "hasOutputParser": true,
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 2.2,
      "position": [
        1280,
        144
      ],
      "id": "34a5ff40-7e3c-46fb-bdb9-3597f3c44b72",
      "name": "Regex Writer Agent"
    },
    {
      "parameters": {
        "jsCode": "// Get the XML string from the input\nconst xmlString = $input.first().json.data;\n\n// Extract all URLs using regex\nconst urlMatches = xmlString.match(/<loc>(.*?)<\\/loc>/g) || [];\n\nconst urls = urlMatches.map(match => {\n  return match.replace(/<\\/?loc>/g, '');\n});\n\n// Return as array of items\n// return urls.map(url => ({\n//   json: {\n//     url,\n//     scope:false\n//     // path: url.replace('https://iqbala.com', '') || '/',\n//     // domain: 'iqbala.com'\n//   }\n// }));\nreturn [{ json: { urls } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        64,
        144
      ],
      "id": "2b2e5b40-7f44-4aab-a55b-b94d1358f1f8",
      "name": "Convert Sitemap to JSON"
    },
    {
      "parameters": {
        "url": "={{ $json.sitemap }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -64,
        144
      ],
      "id": "1ee54e9a-0c4f-40ad-9cce-0cdbf97525d4",
      "name": "Get Sitemap"
    },
    {
      "parameters": {
        "model": "openai/gpt-5.4-nano",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        240,
        352
      ],
      "id": "8ba4df90-4a02-4345-829f-ae3e70e4f1c5",
      "name": "OpenRouter Chat Model1",
      "credentials": {
        "openRouterApi": {
          "id": "baaHEuVk7u6Z0Liu",
          "name": "OpenRouter account"
        }
      }
    },
    {
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "123"
      },
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "typeVersion": 1.3,
      "position": [
        336,
        368
      ],
      "id": "aafc03f0-55a5-4c6e-af24-61d31859104f",
      "name": "Simple Memory1"
    },
    {
      "parameters": {
        "jsonSchemaExample": "{\n\t\"output\": [\"https://iqbala.com/\",\"https://iqbala.com/1\"]\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        448,
        368
      ],
      "id": "70cdaead-6e62-4a80-ac2d-9341d976f196",
      "name": "Structured Output Parser1"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=I am creating a Regex. \n\nOutput comma-separated URLs from the URL list that are in scope to be be changed.\n\n%INPUT%\nURL_LIST: https://mysite.com/,https://mysite.com/page,https://mysite.com/page2,https://mysite.com/page/sub-page\nTARGET_URL: https://mysite.com/page\nCHANGE_REQUEST: Change /page to /page-alt\n\n%OUTPUT%\nhttps://mysite.com/page,https://mysite.com/page/sub-page\n\n%INPUT%\nURL_LIST: {{ $json.urls }}\n\nTARGET_URL: {{ $('Edit Fields').item.json.url }}\nCHANGE_REQUEST: {{ $('Edit Fields').item.json.change_request }}\n\n%OUTPUT%\n",
        "hasOutputParser": true,
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 2.2,
      "position": [
        256,
        144
      ],
      "id": "fa2fa7ec-7ec4-40a9-b248-8817b23db4c7",
      "name": "Scope URL list"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "2de82f54-1f1c-40e6-b407-47cb7fc990cd",
              "name": "failures",
              "value": "={{ $('Regex Validator').item.json.failures }}",
              "type": "array"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2160,
        160
      ],
      "id": "7a3e56d2-ea65-4468-974d-63afd2918aaa",
      "name": "Format error"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        1088,
        144
      ],
      "id": "f75e8a01-0f37-4b0c-9a7b-3f9a8a604f51",
      "name": "Merge"
    },
    {
      "parameters": {
        "jsCode": "// Hard-coded URL list with scope\nconst urls = $('URL scope check').first().json.urls;\n\n// Regex params from agent output, test URL from Edit Fields\nconst { match, replace = '', flags = '' } = $('Regex Writer Agent').first().json.output;\nconst testUrl = $('Edit Fields').first().json.url;\n\n/** True if pattern uses JS lookbehind (?<= or (?<!), respecting \\\\ escapes and [...] classes. */\nfunction hasLookbehind(pattern) {\n  let i = 0;\n  let inClass = false;\n\n  while (i < pattern.length) {\n    const c = pattern[i];\n\n    if (c === '\\\\' && i + 1 < pattern.length) {\n      i += 2;\n      continue;\n    }\n\n    if (inClass) {\n      if (c === ']') inClass = false;\n      i++;\n      continue;\n    }\n\n    if (c === '[') {\n      inClass = true;\n      i++;\n      continue;\n    }\n\n    if (c === '(' && (pattern.startsWith('(?<=', i) || pattern.startsWith('(?<!', i))) {\n      return true;\n    }\n\n    i++;\n  }\n\n  return false;\n}\n\n/**\n * Finds all negative lookaheads `(?!...)` anywhere in the pattern (not just\n * leading), respecting escapes, nested parens, and character classes.\n * Returns an array of the exact substrings found, e.g. [\"(?!\\\\/)\"].\n * We report rather than strip so the offending construct can be surfaced\n * back to the generating model for a proper fix, instead of silently\n * papering over a recurring mistake.\n */\nfunction findNegativeLookaheads(pattern) {\n  const found = [];\n  let i = 0;\n\n  while (i < pattern.length) {\n    const c = pattern[i];\n\n    if (c === '\\\\' && i + 1 < pattern.length) {\n      i += 2;\n      continue;\n    }\n\n    if (c === '[') {\n      // skip over character class without treating its contents as pattern syntax\n      let j = i + 1;\n      while (j < pattern.length) {\n        if (pattern[j] === '\\\\' && j + 1 < pattern.length) {\n          j += 2;\n          continue;\n        }\n        if (pattern[j] === ']') break;\n        j++;\n      }\n      i = j + 1;\n      continue;\n    }\n\n    if (c === '(' && pattern.startsWith('(?!', i)) {\n      let depth = 0;\n      let inClass = false;\n      let j = i;\n      let end = -1;\n\n      while (j < pattern.length) {\n        const cj = pattern[j];\n        if (cj === '\\\\' && j + 1 < pattern.length) {\n          j += 2;\n          continue;\n        }\n        if (inClass) {\n          if (cj === ']') inClass = false;\n          j++;\n          continue;\n        }\n        if (cj === '[') {\n          inClass = true;\n          j++;\n          continue;\n        }\n        if (cj === '(') depth++;\n        if (cj === ')') {\n          depth--;\n          if (depth === 0) {\n            end = j;\n            break;\n          }\n        }\n        j++;\n      }\n\n      if (end === -1) {\n        // Unbalanced parens — stop scanning and let RegExp surface the syntax error.\n        break;\n      }\n\n      found.push(pattern.slice(i, end + 1));\n      i = end + 1;\n      continue;\n    }\n\n    i++;\n  }\n\n  return found;\n}\n\n/**\n * Finds raw whitespace characters (space, tab, newline, carriage return)\n * anywhere in the pattern, whether bare or backslash-escaped (escaping a\n * space is a no-op in JS regex — `\\ ` and ` ` match identically). URLs never\n * contain literal whitespace, so any occurrence here is almost certainly a\n * stray character the model accidentally inserted (e.g. between capture\n * groups: \"(\\/?) (\\/)?\") rather than an intentional match target. If a\n * pattern genuinely needs to match whitespace, it should use `\\s`, `\\t`, etc.\n * — those are reported separately and are NOT flagged here since they don't\n * contain a literal whitespace byte.\n * Returns an array of { index, context } for each stray whitespace found.\n */\nfunction findStrayWhitespace(pattern) {\n  const found = [];\n  let i = 0;\n\n  while (i < pattern.length) {\n    const c = pattern[i];\n\n    // A backslash-escaped whitespace char (e.g. \"\\ \") still contains a raw\n    // whitespace byte at i+1 — flag that position, then skip both chars.\n    if (c === '\\\\' && i + 1 < pattern.length) {\n      const next = pattern[i + 1];\n      if (next === ' ' || next === '\\t' || next === '\\n' || next === '\\r') {\n        const start = Math.max(0, i - 10);\n        const end = Math.min(pattern.length, i + 12);\n        found.push({ index: i, context: pattern.slice(start, end) });\n      }\n      i += 2;\n      continue;\n    }\n\n    if (c === ' ' || c === '\\t' || c === '\\n' || c === '\\r') {\n      const start = Math.max(0, i - 10);\n      const end = Math.min(pattern.length, i + 10);\n      found.push({ index: i, context: pattern.slice(start, end) });\n    }\n\n    i++;\n  }\n\n  return found;\n}\n\n// --- Regex validation ---\nfunction validateRegex(match, flags) {\n  if (typeof match !== 'string' || match.length === 0) {\n    return { ok: false, error: '`match` must be a non-empty string.' };\n  }\n  if (typeof flags !== 'string') {\n    return { ok: false, error: '`flags` must be a string.' };\n  }\n\n  const strayWhitespace = findStrayWhitespace(match);\n  if (strayWhitespace.length > 0) {\n    const details = strayWhitespace\n      .map((w) => `index ${w.index} near \"...${w.context}...\"`)\n      .join('; ');\n    return {\n      ok: false,\n      error:\n        `Stray whitespace character(s) found in \\`match\\`, which URLs never ` +\n        `contain and will break matching: ${details}. Remove the whitespace — ` +\n        `capture groups must be written back-to-back with no space between them ` +\n        `(e.g. \"(\\\\/?)(\\\\/)?(\\\\?.*)?(#.*)?$\", NOT \"(\\\\/?) (\\\\/)?(\\\\?.*)?(#.*)?$\"). ` +\n        `Regenerate the match with the groups concatenated directly.`,\n    };\n  }\n\n  const negativeLookaheads = findNegativeLookaheads(match);\n  if (negativeLookaheads.length > 0) {\n    return {\n      ok: false,\n      error:\n        `Negative lookahead(s) are not allowed: ${negativeLookaheads.join(', ')}. ` +\n        `Remove them — the trailing $ anchor combined with (\\\\/?)(\\\\/)?(\\\\?.*)?(#.*)?$ ` +\n        `already guarantees the match cannot bleed into a longer path segment ` +\n        `(e.g. \"\\\\/apps(\\\\/?)(\\\\/)?(\\\\?.*)?(#.*)?$\" cannot match inside \"/apps2\" or ` +\n        `\"/appstore\", because $ has nothing left to consume). Regenerate the match ` +\n        `using only the standard template capture groups — no lookahead needed.`,\n    };\n  }\n\n  if (hasLookbehind(match)) {\n    return {\n      ok: false,\n      error:\n        'Lookbehind (`(?<=...)` or `(?<!...)`) is not allowed. Use a JavaScript-compatible pattern (e.g. capturing groups) instead.',\n    };\n  }\n\n  try {\n    const regex = new RegExp(match, flags);\n    return { ok: true, regex };\n  } catch (e) {\n    return { ok: false, error: `Invalid regular expression: ${e?.message ?? String(e)}` };\n  }\n}\n\nconst regexResult = validateRegex(match, flags);\nif (!regexResult.ok) {\n  return [{ json: { ok: false, error: regexResult.error } }];\n}\n\nconst regex = regexResult.regex;\nconst agentUrlMatched = regex.test(testUrl);\nconst agentUrlReplaced = agentUrlMatched ? testUrl.replace(regex, replace) : null;\n\nconst failures = [];\nfor (const entry of urls) {\n  const { url, scope } = entry;\n  const entryErrors = [];\n  let matched = false;\n  let replaced = null;\n\n  try {\n    matched = regex.test(url);\n  } catch (e) {\n    entryErrors.push(`Regex test failed: ${e?.message ?? String(e)}`);\n  }\n\n  try {\n    replaced = url.replace(regex, replace);\n  } catch (e) {\n    entryErrors.push(`String.replace failed: ${e?.message ?? String(e)}`);\n  }\n\n  if (scope && !matched) entryErrors.push(`In-scope URL was not matched by regex: \"${url}\"`);\n  if (!scope && matched) entryErrors.push(`Out-of-scope URL was unexpectedly matched: \"${url}\"`);\n\n  if (entryErrors.length > 0) {\n    failures.push({ url, scope, errors: entryErrors, matched, replaced: matched ? replaced : null });\n  }\n}\n\nif (failures.length === 0) {\n  return [\n    {\n      json: {\n        ok: true,\n        agentUrl: { url: testUrl, matched: agentUrlMatched, replaced: agentUrlReplaced },\n      },\n    },\n  ];\n}\n\nreturn [\n  {\n    json: {\n      ok: false,\n      agentUrl: { url: testUrl, matched: agentUrlMatched, replaced: agentUrlReplaced },\n      failures,\n    },\n  },\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1664,
        144
      ],
      "id": "35206538-7d2c-4652-9a7b-4b296011e79b",
      "name": "Regex Validator"
    },
    {
      "parameters": {
        "jsCode": "const sitemapUrls = $('Convert Sitemap to JSON').first().json.urls;\nconst agentOutput = $input.first().json.output;\n\n/**\n * Normalizes a URL for scope comparison by stripping a single trailing\n * slash from the path portion (before any query string or hash), so that\n * \"/apps\" and \"/apps/\" are treated as equivalent — mirroring the\n * trailing-slash-agnostic matching the Regex Writer Agent is required to\n * produce (see the mandatory capture-group suffix rule in its prompt).\n * Query strings and hashes are left untouched; only the base path is\n * affected. Does not collapse multiple trailing slashes beyond one, and\n * leaves single-character bases (e.g. a bare \"/\") alone to avoid producing\n * an empty string.\n */\nfunction normalizeUrl(url) {\n  const str = String(url);\n  const m = str.match(/^([^?#]*)([?#].*)?$/);\n  let base = m ? m[1] : str;\n  const rest = m && m[2] ? m[2] : '';\n\n  if (base.length > 1 && base.endsWith('/')) {\n    base = base.slice(0, -1);\n  }\n\n  return base + rest;\n}\n\nconst agentUrls = Array.isArray(agentOutput)\n  ? agentOutput.flatMap(s => String(s).split(/[,\\r\\n]+/))\n  : String(agentOutput ?? '').split(/[,\\r\\n]+/);\n\nconst agentUrlSet = new Set(\n  agentUrls\n    .map(s => s.trim())\n    .filter(Boolean)\n    .map(normalizeUrl)\n);\n\nconst result = sitemapUrls.map((url) => ({\n  url, // keep original, unmodified — normalization is only for comparison\n  scope: agentUrlSet.has(normalizeUrl(url)),\n}));\n\nreturn [{ json: { urls: result } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        512,
        144
      ],
      "id": "75c2e815-f738-43c5-af51-68852b94f463",
      "name": "URL scope check"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "ca626990-11d4-4a37-b6e8-10524051738b",
              "name": "match",
              "value": "={{ $('Regex Writer Agent').item.json.output.match }}",
              "type": "string"
            },
            {
              "id": "c52ecd58-b1ba-4e7a-b9d0-a2ce0bd68135",
              "name": "replace",
              "value": "={{ $('Regex Writer Agent').item.json.output.replace.replace(/\\\\/g, '') }}",
              "type": "string"
            },
            {
              "id": "b2ad0448-ce1f-4fd7-95a1-05c7c9b57457",
              "name": "test_name",
              "value": "={{ $json.text }}",
              "type": "string"
            },
            {
              "id": "89c8721a-7b0f-45ec-b5b8-2506da72aaa6",
              "name": "account_id",
              "value": 100414230,
              "type": "number"
            },
            {
              "id": "315a2e4f-ca03-4775-9f46-cd6c27c949a9",
              "name": "project_id",
              "value": 100416952,
              "type": "number"
            },
            {
              "id": "ca4f796f-d3c8-4c6e-9870-b3ed456ab850",
              "name": "target_url",
              "value": "={{ $('Edit Fields').item.json.url }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2608,
        128
      ],
      "id": "b7d47967-1ec1-476a-bf33-460f545e61f4",
      "name": "Final output"
    },
    {
      "parameters": {
        "formTitle": "Page redirect Javascript generator",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Target URL",
              "placeholder": "Page URL to redirect",
              "requiredField": true
            },
            {
              "fieldLabel": "Change Request",
              "fieldType": "textarea",
              "placeholder": "Describe the change to the URL in simple terms",
              "requiredField": true
            },
            {
              "fieldLabel": "Sitemap",
              "placeholder": "Link to sitemap",
              "requiredField": true
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.3,
      "position": [
        -400,
        144
      ],
      "id": "65d2ace0-e9cf-4592-977f-9a544d50cce2",
      "name": "On form submission",
      "webhookId": "a0ffb86a-70d6-4f69-84b1-3c6fecc71286"
    },
    {
      "parameters": {
        "model": "openai/gpt-5.4-nano",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        2336,
        320
      ],
      "id": "a172f17c-2f4b-4850-b592-693f08468e5d",
      "name": "OpenRouter Chat Model3",
      "credentials": {
        "openRouterApi": {
          "id": "baaHEuVk7u6Z0Liu",
          "name": "OpenRouter account"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "ebb0c0ac-7adb-4483-96ba-ecaf5434d748",
              "leftValue": "={{ $json.ok }}",
              "rightValue": "false",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            },
            {
              "id": "791b4063-66a7-42f6-9565-b40b9b9f3b41",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 5,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "or"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1792,
        144
      ],
      "id": "9288c8f6-7c66-474a-b79f-3882e2aa4c59",
      "name": "If Regex Validated"
    },
    {
      "parameters": {
        "content": "## Force required information\n",
        "height": 528,
        "width": 288,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -400,
        0
      ],
      "typeVersion": 1,
      "id": "68196b4d-692a-4d36-a080-45713ddea6f8",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "## Write Regex",
        "height": 528,
        "width": 336,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1248,
        0
      ],
      "typeVersion": 1,
      "id": "f100fb57-7692-4e4d-8ffc-796dbde2936f",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Create a name for this experiment. Less than 5 words.\n\nTest type: Redirect test\nTarget URL:   {{ $('Edit Fields').item.json.url }}\nChange request: {{ $('Edit Fields').item.json.change_request }}\n\nName:",
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2320,
        128
      ],
      "id": "60ccb10e-2f4d-4551-a3fd-cc826c3f66b5",
      "name": "Set experiment name"
    },
    {
      "parameters": {
        "content": "## Set name and variables",
        "height": 528,
        "width": 432,
        "color": 3
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2208,
        0
      ],
      "typeVersion": 1,
      "id": "b4e95dfb-43cf-48c7-948b-c250771cfa5d",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "content": "## Get all URLs",
        "height": 528,
        "width": 288,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -96,
        0
      ],
      "typeVersion": 1,
      "id": "69c67570-d5bf-4a73-801b-97648e95d093",
      "name": "Sticky Note4"
    },
    {
      "parameters": {
        "content": "## Mark scoped URLs",
        "height": 528,
        "width": 448,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        208,
        0
      ],
      "typeVersion": 1,
      "id": "321c767a-16a4-4fcf-9ed8-6bd329cd3088",
      "name": "Sticky Note5"
    },
    {
      "parameters": {
        "content": "## QA Regex",
        "height": 528,
        "width": 464,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1600,
        0
      ],
      "typeVersion": 1,
      "id": "1cc02600-35bf-4731-acf4-46cc8bb60434",
      "name": "Sticky Note6"
    },
    {
      "parameters": {
        "model": "openai/gpt-5.4-nano",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        736,
        352
      ],
      "id": "c0b71d29-3694-4ad6-99ab-513a6dcf222a",
      "name": "OpenRouter Chat Model2",
      "credentials": {
        "openRouterApi": {
          "id": "baaHEuVk7u6Z0Liu",
          "name": "OpenRouter account"
        }
      }
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Take a simple user change request, write a detailed document for writing regular expressions that applies to URLs that have scope true, and not those that are false.\n\nTo do this, check for rules you can apply to write a plain languageset of rules that can be used to generate the final regular expression.\n\n%SCOPE%\n{{ $('URL scope check').item.json.urls }}\n\n%CHANGE REQUEST% \n\n{{ $('Edit Fields').item.json.change_request }}\n\n\n%DETAILED SPECIFICATION%\n",
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        736,
        144
      ],
      "id": "6e417a9a-78da-43bc-8796-cab9c3940eb7",
      "name": "Write specification"
    },
    {
      "parameters": {
        "content": "## Write specification",
        "height": 528,
        "width": 304,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        688,
        0
      ],
      "typeVersion": 1,
      "id": "f46ed0d8-8be9-4d31-a7ed-d0c4102e004b",
      "name": "Sticky Note7"
    }
  ],
  "pinData": {
    "On form submission": [
      {
        "json": {
          "Target URL": "https://iqbala.com/blog",
          "Change Request": "Change blog to articles",
          "Sitemap": "https://iqbala.com/sitemap-0.xml",
          "submittedAt": "2026-07-27T13:46:48.001+01:00",
          "formMode": "test"
        }
      }
    ],
    "Edit Fields": [
      {
        "json": {
          "url": "https://iqbala.com/blog",
          "change_request": "Change blog to articles",
          "sitemap": "https://iqbala.com/sitemap-0.xml"
        }
      }
    ]
  },
  "connections": {
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Get Sitemap",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Regex Writer Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory": {
      "ai_memory": [
        [
          {
            "node": "Regex Writer Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Regex Writer Agent",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Convert Sitemap to JSON": {
      "main": [
        [
          {
            "node": "Scope URL list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Sitemap": {
      "main": [
        [
          {
            "node": "Convert Sitemap to JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "Scope URL list",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory1": {
      "ai_memory": [
        [
          {
            "node": "Scope URL list",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser1": {
      "ai_outputParser": [
        [
          {
            "node": "Scope URL list",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Regex Writer Agent": {
      "main": [
        [
          {
            "node": "Regex Validator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scope URL list": {
      "main": [
        [
          {
            "node": "URL scope check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format error": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Regex Writer Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Regex Validator": {
      "main": [
        [
          {
            "node": "If Regex Validated",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "URL scope check": {
      "main": [
        [
          {
            "node": "Write specification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On form submission": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model3": {
      "ai_languageModel": [
        [
          {
            "node": "Set experiment name",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Final output": {
      "main": [
        []
      ]
    },
    "If Regex Validated": {
      "main": [
        [
          {
            "node": "Set experiment name",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set experiment name": {
      "main": [
        [
          {
            "node": "Final output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model2": {
      "ai_languageModel": [
        [
          {
            "node": "Write specification",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Write specification": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "865a3e75-398e-4d69-890c-4ccba6188735",
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "a7d3c8fee44ed6dfa5d3bf2eca37e2240651cc218f5d2ad518615bd97f803884"
  },
  "id": "4bsTRAloQKNDba9D",
  "tags": []
}