{
  "nodes": [
    {
      "id": "4eeb975a-4c11-4d71-b6c0-9ae690c2d706",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1456,
        -720
      ],
      "parameters": {
        "color": 7,
        "width": 1024,
        "height": 208,
        "content": "## Need more advanced automation solutions? Contact us for custom enterprise workflows!\n\n# Growth-AI.fr\n\n## https://www.linkedin.com/in/allanvaccarizi/\n## https://www.linkedin.com/in/hugo-marinier-%F0%9F%A7%B2-6537b633/"
      },
      "typeVersion": 1
    },
    {
      "id": "afa50e9f-5614-48b2-8ecf-a7ef605c2d9c",
      "name": "Sticky Note16",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1456,
        -1136
      ],
      "parameters": {
        "color": 7,
        "width": 1024,
        "height": 400,
        "content": "![Logo Growth AI](https://cdn.prod.website-files.com/6825df5b20329ba581df4914/68d413c43f8729fa336568a6_Logo_horizontal.png)"
      },
      "typeVersion": 1
    },
    {
      "id": "ecd1bf9e-13cc-4b14-be99-9aa193e34608",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        896,
        -464
      ],
      "parameters": {
        "width": 480,
        "height": 720,
        "content": "## AI Workflow design generator\n\n### How it works\n\n1. A user submits a form describing the workflow they want to build.\n2. An AI agent interprets the user's intent and structures the request into a parsed JSON specification.\n3. A second AI agent generates a full n8n workflow definition from the specification, which is validated and variables are set.\n4. A third AI agent analyzes the workflow and groups nodes into logical clusters, then computes their canvas layout and sticky note annotations.\n5. A fourth AI agent renames all nodes with clear, descriptive labels.\n6. The finalized workflow is published directly to an n8n instance via the API.\n\n### Setup steps\n\n- - [ ] Configure Anthropic API credentials for all four LLM sub-nodes (Anthropic Chat Model, LLM \u2014 G\u00e9n\u00e9rateur, LLM \u2014 Groupes, LLM \u2014 Renommage).\n- - [ ] Set your n8n instance URL in the HTTP Request node (\"HTTP Request - Cr\u00e9er Workflow n8n API2\") \u2014 replace `Your n8n url` with your actual instance URL.\n- - [ ] Add your n8n API key to the HTTP Request node for authentication.\n- - [ ] Customize the form fields in \"On form submission\" to capture the workflow description from users.\n- - [ ] Review and adjust the MAX_RETRIES and renameNodes variables in \"Set Workflow Variables\" as needed.\n\n### Customization\n\nYou can adjust the system prompts inside each AI Agent to change how the workflow is generated, how nodes are grouped, or how nodes are named. The MAX_RETRIES variable in \"Set Workflow Variables\" controls retry behavior for JSON validation failures."
      },
      "typeVersion": 1
    },
    {
      "id": "e54a5b4c-9536-44ab-b701-fe83eb5f6d5e",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1456,
        -448
      ],
      "parameters": {
        "color": 7,
        "width": 768,
        "height": 496,
        "content": "## Form trigger and intent parsing\n\nCaptures the user's workflow request via a form, passes it to an AI agent that interprets the intent and structures it into a parsed JSON specification ready for generation."
      },
      "typeVersion": 1
    },
    {
      "id": "341628f6-5487-4c32-942a-78208b65ebca",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2256,
        -448
      ],
      "parameters": {
        "color": 7,
        "width": 544,
        "height": 592,
        "content": "## AI workflow generation and validation\n\nAn AI agent generates a complete n8n workflow JSON from the parsed specification, which is then validated and parsed by a custom code node to ensure correctness."
      },
      "typeVersion": 1
    },
    {
      "id": "fce3e06c-2726-480b-a688-00aa0c8a9ea7",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2864,
        -464
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 336,
        "content": "## Set variables and prepare data\n\nSets key workflow variables (workflow definition, MAX_RETRIES, renameNodes flag) and prepares and parses the data structure before passing it to the grouping agent."
      },
      "typeVersion": 1
    },
    {
      "id": "428d8d20-c20c-45e4-a4f5-cdcf98711b8a",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3296,
        -464
      ],
      "parameters": {
        "color": 7,
        "width": 544,
        "height": 624,
        "content": "## Node grouping and canvas layout\n\nAn AI agent clusters workflow nodes into logical groups and a structured output parser enforces the schema; a code node then computes canvas positions and generates sticky note annotations."
      },
      "typeVersion": 1
    },
    {
      "id": "5b15f709-c0ac-4adf-b85c-6f17ab82807b",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3888,
        -448
      ],
      "parameters": {
        "color": 7,
        "width": 544,
        "height": 608,
        "content": "## AI node renaming\n\nPrepares node data for renaming, then an AI agent generates clear descriptive labels for every node with a structured output parser enforcing the rename schema."
      },
      "typeVersion": 1
    },
    {
      "id": "e3f438c1-4da0-4f5b-b99b-6e52bbd783cb",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4464,
        -464
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 336,
        "content": "## Apply renames and publish workflow\n\nApplies all AI-generated node renames to the final workflow JSON, then publishes the completed workflow to an n8n instance via a POST request to the n8n API."
      },
      "typeVersion": 1
    },
    {
      "id": "8e1776ee-b5a2-4fbb-8310-177f2142c48f",
      "name": "Workflow Generator Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2304,
        -288
      ],
      "parameters": {
        "text": "=Client Brief #{{ $json.workflow_id }} \u2014 {{ $json.client }}\n\nWorkflow Title: {{ $json.titre }}\n\nClient Note (must be prominently displayed):\n{{ $json.note_client }}\n\nBusiness Description:\n{{ $json.description_metier }}\n\nTools identified during the audit: {{ $json.outils_mentionnes.join(', ') }}\n\nConditional Logic: {{ $json.logique_conditionnelle }}\n\nDetailed step-by-step process:\n{{ $json.brief_complet }}\n\nGenerate the corresponding schematic n8n JSON workflow based on this brief.\n\nReminder:\n\n* Naming must remain business-oriented\n* Include 6\u201312 representative nodes\n* Visual clarity and presentation quality take priority over immediate executability\n\nRespond ONLY with the raw JSON object, without markdown or introduction.",
        "options": {
          "systemMessage": "# Client-Facing n8n Workflow Generator\n\nYou are an n8n automation expert. Your mission is to transform a business brief into a **schematic and visual** n8n JSON workflow intended to be presented to a non-technical client.\n\n## Objective\n\nThis workflow is used to **visually present** the automation process to the client during a sales meeting. Readability and business clarity take priority over immediate technical executability.\n\n## Naming conventions (MANDATORY)\n\n* Format: `[Tool/Action] \u2014 [Business description]`\n\n### Correct examples :\n\n* Scheduler \u2014 Daily launch at 8 AM\n* Scraper \u2014 Regional business news monitoring\n* Lusha \u2014 Email + mobile enrichment\n* AI Agent \u2014 Lead analysis and scoring\n* IF \u2014 New seller detected\n* Else \u2014 Unqualified lead\n* Teams \u2014 Fund manager notification\n* Gmail \u2014 Day-0 outreach email\n* Airtable \u2014 Add lead to database\n\n## Available node types\n\n**Triggers:**\n\n* `n8n-nodes-base.scheduleTrigger` \u2014 Cron scheduling\n* `n8n-nodes-base.webhook` \u2014 HTTP webhook\n* `n8n-nodes-base.manualTrigger` \u2014 Manual\n\n**Actions:**\n\n* `n8n-nodes-base.httpRequest` \u2014 API calls (scraping, enrichment, public data)\n* `n8n-nodes-base.code` \u2014 Data processing and transformation\n* `n8n-nodes-base.set` \u2014 Data formatting\n* `n8n-nodes-base.googleSheets` \u2014 Google Sheets\n* `n8n-nodes-base.gmail` \u2014 Gmail\n* `n8n-nodes-base.microsoftTeams` \u2014 Microsoft Teams\n* `n8n-nodes-base.airtable` \u2014 Airtable\n* `n8n-nodes-base.slack` \u2014 Slack\n* `n8n-nodes-base.notion` \u2014 Notion\n* `n8n-nodes-base.hubspot` \u2014 HubSpot\n\n**Logic:**\n\n* `n8n-nodes-base.if` \u2014 If/else condition\n* `n8n-nodes-base.switch` \u2014 Multi-case routing\n* `n8n-nodes-base.filter` \u2014 Data filtering\n* `n8n-nodes-base.merge` \u2014 Flow merging\n* `n8n-nodes-base.splitInBatches` \u2014 Batch looping\n\n**AI:**\n\n* `@n8n/n8n-nodes-langchain.agent` \u2014 AI Agent\n* `@n8n/n8n-nodes-langchain.lmChatAnthropic` \u2014 Claude\n* `@n8n/n8n-nodes-langchain.lmChatOpenAI` \u2014 OpenAI\n\n**Non-native APIs \u2192 use `n8n-nodes-base.httpRequest` with an explicit name**\n\n## Structure rules\n\n* **Linear left-to-right** flow (visual priority)\n* Vertical branches for conditions (top = positive path, bottom = alternative path)\n* Trigger always as the first node\n* Between 6 and 12 representative nodes (neither too simple nor overloaded)\n* Every important step from the brief must be visible as a distinct node\n\n## Positioning\n\n* X: increment by 280\u2013320 per horizontal step\n* Y: main flow at 300, upper branch at 100, lower branch at 500\n* First node (trigger): position [0, 300]\n\n## UUID format (MANDATORY)\n\nEach node must have a valid UUID v4 formatted \"id\":\nxxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\n\n* x = hexadecimal character [0-9a-f]\n* The 3rd segment MUST start with 4\n* The 4th segment MUST start with 8, 9, a, or b\n\nValid examples:\n\n* \"550e8400-e29b-41d4-a716-446655440000\"\n* \"6ba7b810-9dad-41d1-80b4-00c04fd430c8\"\n* \"f47ac10b-58cc-4372-a567-0e02b2c3d479\"\n\n## Connection format (MANDATORY)\n\nEach connection MUST use this exact format with one object per target:\n\nSimple connection:\n\"Source node name\": {\n\"main\": [\n[{\"node\": \"Target node name\", \"type\": \"main\", \"index\": 0}]\n]\n}\n\nIF connection (2 outputs \u2014 true on top, false on bottom):\n\"IF node name\": {\n\"main\": [\n[{\"node\": \"True branch node\", \"type\": \"main\", \"index\": 0}],\n[{\"node\": \"False branch node\", \"type\": \"main\", \"index\": 0}]\n]\n}\n\nSTRICTLY FORBIDDEN:\n\n* \"main\": [[\"Node name\"]]  \u2190 direct strings, invalid\n* \"main\": [[]]             \u2190 empty arrays, invalid\n* Omitting \"type\" or \"index\" \u2190 mandatory fields\n\n## Output format (STRICT)\n\nRespond ONLY with the raw JSON object. No ```json, no text before or after, no line breaks inside strings.\n\n{\n\"name\": \"[Business workflow title \u2014 Client]\",\n\"nodes\": [\n{\n\"parameters\": {},\n\"id\": \"[valid uuid v4]\",\n\"name\": \"[French name]\",\n\"type\": \"[exact type]\",\n\"typeVersion\": 1,\n\"position\": [x, y]\n}\n],\n\"connections\": {\n\"Source node name\": {\n\"main\": [[{\"node\": \"Target node name\", \"type\": \"main\", \"index\": 0}]]\n}\n},\n\"pinData\": {},\n\"meta\": {\"instanceId\": \"generated-workflow\"}\n}\n"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "67494ba6-8790-435d-b4fe-022f3ac38107",
      "name": "Claude Model for Generator",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        2304,
        0
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": {}
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "f53df6cc-9e9d-4d73-8199-7a782be521eb",
      "name": "Parse and Validate JSON",
      "type": "n8n-nodes-base.code",
      "position": [
        2656,
        -288
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "try {\n  const input = $input.item.json;\n  let workflowData = input.output || input;\n  if (typeof workflowData === 'string') {\n    workflowData = workflowData\n      .replace(/```json\\n?/g, '')\n      .replace(/```\\n?/g, '')\n      .trim();\n    workflowData = JSON.parse(workflowData);\n  }\n  if (!workflowData.name || !workflowData.nodes || !workflowData.connections) {\n    throw new Error('JSON workflow invalide : propri\u00e9t\u00e9s manquantes (name, nodes, connections)');\n  }\n  workflowData.settings = workflowData.settings || { executionOrder: 'v1' };\n  workflowData.staticData = workflowData.staticData || null;\n  workflowData.pinData = workflowData.pinData || {};\n  workflowData.meta = workflowData.meta || { instanceId: 'generated-workflow' };\n  return workflowData;\n} catch (error) {\n  throw new Error(`Erreur parsing JSON workflow : ${error.message}`);\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "a7167724-f087-45e2-870e-c2c9e60f3549",
      "name": "Set Workflow Config Variables",
      "type": "n8n-nodes-base.set",
      "position": [
        2912,
        -288
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "set-wf-001",
              "name": "workflow",
              "type": "object",
              "value": "={{ $json }}"
            },
            {
              "id": "set-wf-002",
              "name": "MAX_RETRIES",
              "type": "number",
              "value": 3
            },
            {
              "id": "set-wf-003",
              "name": "renameNodes",
              "type": "boolean",
              "value": true
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "c426dfa0-c340-4004-85da-4f853f4757cf",
      "name": "Logical Groups Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        3344,
        -288
      ],
      "parameters": {
        "text": "=Client Workflow: {{ $json.workflowName }}\nNode Count: {{ $json.nodeCount }}\n\nNodes with positions, connections, and context:\n{{ JSON.stringify($json.nodes, null, 2) }}\n\nGroup these nodes by taking into account both their business role and their spatial position on the canvas.\nEach group should correspond to a distinct step or phase of the client process.\nEach node ID must appear in exactly one group.",
        "options": {
          "systemMessage": "You analyze an n8n workflow to create clear visual documentation intended to be presented to a non-technical client. \n\n## Your mission\n\n1. Group nodes by logical business step AND spatial proximity\n2. Generate a high-level overview describing the business value of the workflow\n3. Write group titles in client-friendly language\n\n## Spatial awareness (CRITICAL)\n\nThe nodes have [x, y] positions on a 2D canvas. The creator placed them intentionally.\n\n* Group nodes that are both logically related AND spatially close\n* If two nodes do similar things but are far apart (>800px on one axis), prefer separate groups\n* An isolated node may form its own group if it is far from others\n\n## Grouping rules\n\n* Target at least {{ Math.ceil((Number($json.nodeCount) || 0) / 3) }} groups depending on complexity\n* Each node must belong to exactly ONE group\n* Small, dense groups are better than large scattered groups\n\n## Naming (CLIENT-ORIENTED)\n\n* Short titles, 3\u20136 words, first letter capitalized\n* Focus on what it does FOR the business, not how it works technically\n* If the workflow language is French, examples:\n\n  * \"D\u00e9clenchement automatique\"\n  * \"D\u00e9tection des cessions\"\n  * \"Enrichissement des contacts\"\n  * \"Qualification du lead\"\n  * \"Envoi email personnalis\u00e9\"\n  * \"Notification \u00e9quipe commerciale\"\n* If the workflow language is English, examples:\n\n  * \"Automatic Triggering\"\n  * \"Seller Detection\"\n  * \"Contact Enrichment\"\n  * \"Lead Qualification\"\n  * \"Personalized Email Outreach\"\n  * \"Sales Team Notification\"\n\n## High-level overview\n\n* **howItWorks**: 3\u20135 short paragraphs in client-friendly language focused on concrete business value. Explain what this automation helps them gain (time, money, detected opportunities, avoided errors). No technical jargon, no mention of nodes or APIs. Describe the before/after: what the client previously handled manually and what now happens automatically.\n\n  * In French, start with: \"Ce workflow vous permet de...\"\n  * In English, start with: \"This workflow allows you to...\"\n* **setupSteps**: Return an empty array []\n* **customization**: Optional customization notes in the detected language. Only if truly relevant, otherwise return an empty string.\n\n## Output rules (CRITICAL)\n\n* Copy node IDs EXACTLY as they appear in the input \u2014 character by character, without modification\n* Never rephrase, shorten, or reconstruct an ID\n* If an input ID is \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\", it must appear identically in your output\n* Every input node ID must appear in exactly one group\n* Order groups according to the typical execution flow (trigger/input first)"
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3
    },
    {
      "id": "d543d4f1-2d8c-40f7-8c72-5bc8829daf58",
      "name": "Claude Model for Groups",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        3344,
        -32
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": {}
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "3bff05bd-7201-49fe-9f28-faa8dc04d9e5",
      "name": "Groups Structured Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        3408,
        -128
      ],
      "parameters": {
        "autoFix": true,
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"mainOverview\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"howItWorks\": { \"type\": \"string\", \"description\": \"Liste num\u00e9rot\u00e9e 2-5 items expliquant le workflow\" },\n        \"setupSteps\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"Instructions de configuration\" },\n        \"customization\": { \"type\": \"string\", \"description\": \"Notes de personnalisation optionnelles\" }\n      },\n      \"required\": [\"howItWorks\", \"setupSteps\"],\n      \"additionalProperties\": false\n    },\n    \"groups\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"groupId\": { \"type\": \"integer\", \"description\": \"ID s\u00e9quentiel du groupe, commence \u00e0 0\" },\n          \"title\": { \"type\": \"string\", \"description\": \"Titre court, 3-6 mots en fran\u00e7ais\" },\n          \"description\": { \"type\": \"string\", \"description\": \"Description br\u00e8ve ou cha\u00eene vide\" },\n          \"nodeIds\": { \"type\": \"array\", \"items\": { \"type\": \"string\" }, \"description\": \"IDs des nodes dans ce groupe\" }\n        },\n        \"required\": [\"groupId\", \"title\", \"description\", \"nodeIds\"],\n        \"additionalProperties\": false\n      }\n    }\n  },\n  \"required\": [\"mainOverview\", \"groups\"],\n  \"additionalProperties\": false\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "316f2e07-97c7-4ff4-8c6a-921bc7b890a6",
      "name": "Parse Nodes for Renaming",
      "type": "n8n-nodes-base.code",
      "position": [
        3936,
        -288
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const workflow = $input.item.json.workflow;\nconst allNodes = workflow.nodes || [];\nconst connections = workflow.connections || {};\n\nconst isSticky = (node) => node.type === 'n8n-nodes-base.stickyNote';\nconst getSimpleType = (fullType) => { if (!fullType) return 'unknown'; const parts = fullType.split('.'); return parts[parts.length - 1] || fullType; };\nconst safeGet = (obj, path, defaultVal = undefined) => { const keys = path.split('.'); let current = obj; for (const key of keys) { if (current === null || current === undefined) return defaultVal; current = current[key]; } return current !== undefined ? current : defaultVal; };\nconst truncate = (str, maxLen) => { if (!str) return ''; if (str.length <= maxLen) return str; return str.substring(0, maxLen) + '...'; };\n\nconst outgoingConnections = {};\nconst incomingConnections = {};\n\nfor (const [fromName, connDef] of Object.entries(connections)) {\n  if (!connDef) continue;\n  const mains = connDef.main || [];\n  for (const outputArr of mains) {\n    if (!Array.isArray(outputArr)) continue;\n    for (const conn of outputArr) {\n      if (!conn || !conn.node) continue;\n      if (!outgoingConnections[fromName]) outgoingConnections[fromName] = [];\n      outgoingConnections[fromName].push(conn.node);\n      if (!incomingConnections[conn.node]) incomingConnections[conn.node] = [];\n      incomingConnections[conn.node].push(fromName);\n    }\n  }\n  for (const [key, value] of Object.entries(connDef)) {\n    if (key === 'main') continue;\n    if (!Array.isArray(value)) continue;\n    for (const outputArr of value) {\n      if (!Array.isArray(outputArr)) continue;\n      for (const conn of outputArr) {\n        if (!conn || !conn.node) continue;\n        if (!outgoingConnections[fromName]) outgoingConnections[fromName] = [];\n        outgoingConnections[fromName].push(conn.node);\n        if (!incomingConnections[conn.node]) incomingConnections[conn.node] = [];\n        incomingConnections[conn.node].push(fromName);\n      }\n    }\n  }\n}\n\nfunction extractContext(node) {\n  const p = node.parameters || {};\n  const type = getSimpleType(node.type);\n  switch (type) {\n    case 'httpRequest': return { method: p.method || 'GET', url: truncate(p.url, 80), description: `${p.method || 'GET'} vers ${truncate(p.url, 50) || 'URL'}` };\n    case 'set': { const fields = safeGet(p, 'assignments.assignments', []).slice(0, 5).map(a => a.name).filter(Boolean); return { fields, description: `D\u00e9finit : ${fields.join(', ') || 'champs'}` }; }\n    case 'if': return { description: 'Branchement conditionnel' };\n    case 'switch': return { description: 'Aiguillage multi-cas' };\n    case 'code': { const jsCode = p.jsCode || p.code || ''; const firstLine = jsCode.split('\\n')[0] || ''; const isComment = firstLine.trim().startsWith('//'); return { hint: isComment ? firstLine.replace('//', '').trim() : truncate(firstLine, 50), description: isComment ? firstLine.replace('//', '').trim() : 'Code personnalis\u00e9' }; }\n    case 'filter': return { description: 'Filtrage des \u00e9l\u00e9ments' };\n    case 'scheduleTrigger': return { description: 'D\u00e9clenchement planifi\u00e9' };\n    case 'webhook': return { httpMethod: p.httpMethod || 'GET', path: p.path || '/', description: `Webhook ${p.httpMethod || 'GET'} ${p.path || '/'}` };\n    case 'gmail': return { operation: p.operation || 'send', description: `Email via Gmail` };\n    case 'microsoftTeams': return { description: 'Message Microsoft Teams' };\n    case 'airtable': return { operation: p.operation || 'read', description: `Airtable ${p.operation || 'lecture'}` };\n    case 'agent': return { description: 'Agent IA' };\n    default: return { description: `Node ${type}` };\n  }\n}\n\nfunction findExpressionReferences(node) {\n  const refs = [];\n  const jsonStr = JSON.stringify(node.parameters || {});\n  const regex = /\\$\\(['\"]([^'\"]+)['\"]\\)/g;\n  let match;\n  while ((match = regex.exec(jsonStr)) !== null) refs.push(match[1]);\n  return refs;\n}\n\nconst nodesForRenaming = [];\nconst existingNames = [];\n\nfor (const node of allNodes) {\n  if (isSticky(node)) continue;\n  existingNames.push(node.name);\n  const context = extractContext(node);\n  const expressionRefs = findExpressionReferences(node);\n  nodesForRenaming.push({\n    id: node.id, currentName: node.name, type: node.type,\n    simpleType: getSimpleType(node.type), context,\n    incomingFrom: [...new Set(incomingConnections[node.name] || [])],\n    outgoingTo: [...new Set(outgoingConnections[node.name] || [])],\n    hasExpressionRefs: expressionRefs.length > 0, expressionRefs\n  });\n}\n\nreturn { json: { workflowName: workflow.name || 'Workflow client', nodeCount: nodesForRenaming.length, nodes: nodesForRenaming, existingNames } };"
      },
      "typeVersion": 2
    },
    {
      "id": "ad9c06c9-3192-4bde-b4da-98d79bf99dbe",
      "name": "Node Renaming Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        4128,
        -288
      ],
      "parameters": {
        "text": "=Client Workflow: {{ $json.workflowName }}\nNumber of nodes to rename: {{ $json.nodeCount }}\n\nNodes:\n{{ JSON.stringify($json.nodes, null, 2) }}\n\nGenerate a new descriptive name for EVERY node listed above.\nFollow the workflow naming conventions.\nIMPORTANT:\n\n* Every node must receive a new name\n* All names must be unique\n* Maximum 40 characters per name",
        "options": {
          "systemMessage": "You rename n8n workflow nodes to make them understandable for a non-technical client. ALL NAMES MUST MATCH THE LANGUAGE OF THE WORKFLOW (French or English).\n\n## Mandatory format\n\n`[Tool/Category] \u2014 [Business description]`\n\n## Naming conventions by node type\n\n| Type                   | Convention                            | English Examples                   |\n| ---------------------- | ------------------------------------- | ---------------------------------- |\n| scheduleTrigger        | Scheduler/Planificateur \u2014 [frequency] | Scheduler \u2014 Every morning at 8 AM  |\n| webhook                | Trigger/D\u00e9clencheur \u2014 [event]         | Trigger \u2014 Form submission received |\n| manualTrigger          | Trigger/D\u00e9clencheur \u2014 Manual launch   | Trigger \u2014 Manual launch            |\n| httpRequest (scraping) | Scraper \u2014 [source]                    | Scraper \u2014 Regional business news   |\n| httpRequest (API)      | [Service] \u2014 [action]                  | Lusha \u2014 Email + mobile enrichment  |\n| code                   | [Verb] \u2014 [what it does]               | Analyze \u2014 Lead scoring             |\n| if                     | IF/SI \u2014 [condition]                   | IF \u2014 Qualified lead detected       |\n| switch                 | Routing/Aiguillage \u2014 [criteria]       | Routing \u2014 Fund type                |\n| filter                 | Filter/Filtre \u2014 [criteria]            | Filter \u2014 Eligible sellers          |\n| set                    | Data/Donn\u00e9es \u2014 [description]          | Data \u2014 Contact preparation         |\n| gmail                  | Email \u2014 [action]                      | Email \u2014 Personalized outreach J0   |\n| microsoftTeams         | Teams \u2014 [action]                      | Teams \u2014 Sales manager alert        |\n| airtable               | Airtable \u2014 [action]                   | Airtable \u2014 Add lead database       |\n| googleSheets           | Sheets \u2014 [action]                     | Sheets \u2014 Status update             |\n| agent                  | AI Agent/Agent IA \u2014 [role]            | AI Agent \u2014 Content analysis        |\n| splitInBatches         | Loop/Boucle \u2014 [scope]                 | Loop \u2014 For each contact            |\n| merge                  | Merge/Fusion \u2014 [description]          | Merge \u2014 Enrichment results         |\n\n## Rules\n\n1. Names must match the workflow language consistently\n2. Less than 40 characters\n3. UNIQUE names within the workflow (add a suffix if needed: Email \u2014 Follow-up J+14)\n4. BUSINESS-oriented, not technical\n5. Start with a verb or tool name\n6. No special characters except dash (\u2014) and spaces\n\nReturn a rename for EVERY node without exception."
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3
    },
    {
      "id": "88f0340f-b806-4900-b3dd-89d2fe8064ed",
      "name": "Claude Model for Renaming",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        4128,
        -32
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": {}
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "ec0bcb66-e1da-4466-83c1-fa31cfe90af2",
      "name": "Renaming Structured Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        4176,
        -128
      ],
      "parameters": {
        "autoFix": true,
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"renames\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"string\", \"description\": \"L'ID du node\" },\n          \"currentName\": { \"type\": \"string\", \"description\": \"Le nom actuel du node\" },\n          \"newName\": { \"type\": \"string\", \"description\": \"Le nouveau nom descriptif (moins de 40 caract\u00e8res, unique, en fran\u00e7ais)\" }\n        },\n        \"required\": [\"id\", \"currentName\", \"newName\"],\n        \"additionalProperties\": false\n      }\n    }\n  },\n  \"required\": [\"renames\"],\n  \"additionalProperties\": false\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "c77149c6-5bcd-45a6-ab41-f1633532404f",
      "name": "Prepare and Parse Grid Data",
      "type": "n8n-nodes-base.code",
      "position": [
        3120,
        -288
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const GRID = 16;\nconst DEFAULT_SIZE = [96, 96];\nconst CONFIG_SIZE = [80, 80];\nconst CONFIGURABLE_MIN_WIDTH = 256;\n\nconst workflow = $input.item.json.workflow;\nconst allNodes = workflow.nodes || [];\nconst connections = workflow.connections || {};\n\nconst cleanedNodes = allNodes.filter(n => n.type !== 'n8n-nodes-base.stickyNote');\n\nconst AI_CONNECTION_TYPES = new Set(['ai_languageModel', 'ai_tool', 'ai_memory', 'ai_outputParser']);\n\nconst nodesByName = {};\nfor (const node of cleanedNodes) nodesByName[node.name] = node;\n\nconst aiSubNodes = {};\nconst subNodeParents = {};\n\nfor (const [fromName, connDef] of Object.entries(connections)) {\n  if (!connDef) continue;\n  for (const [connType, outputs] of Object.entries(connDef)) {\n    if (!AI_CONNECTION_TYPES.has(connType)) continue;\n    for (const arr of (outputs || [])) {\n      if (!Array.isArray(arr)) continue;\n      for (const c of arr) {\n        if (!c?.node) continue;\n        const parentNode = nodesByName[c.node];\n        const subNode = nodesByName[fromName];\n        if (!parentNode || !subNode) continue;\n        if (!aiSubNodes[parentNode.id]) aiSubNodes[parentNode.id] = [];\n        aiSubNodes[parentNode.id].push({ id: subNode.id, name: subNode.name, type: subNode.type, position: subNode.position });\n        subNodeParents[subNode.id] = parentNode.id;\n      }\n    }\n  }\n}\n\nconst outgoing = {};\nconst incoming = {};\nconst maxInputIndex = {};\nconst maxOutputIndex = {};\n\nfor (const [fromName, connDef] of Object.entries(connections)) {\n  if (!connDef?.main) continue;\n  maxOutputIndex[fromName] = Math.max(maxOutputIndex[fromName] || 0, connDef.main.length);\n  for (let outputIdx = 0; outputIdx < connDef.main.length; outputIdx++) {\n    const outputArr = connDef.main[outputIdx];\n    if (!Array.isArray(outputArr)) continue;\n    for (const conn of outputArr) {\n      if (!conn?.node) continue;\n      if (!outgoing[fromName]) outgoing[fromName] = [];\n      outgoing[fromName].push(conn.node);\n      if (!incoming[conn.node]) incoming[conn.node] = [];\n      incoming[conn.node].push(fromName);\n      const inputIdx = (conn.index ?? 0) + 1;\n      maxInputIndex[conn.node] = Math.max(maxInputIndex[conn.node] || 0, inputIdx);\n    }\n  }\n}\n\nconst isTrigger = (node) => {\n  const t = node.type || '';\n  return t.includes('Trigger') || t.includes('trigger') || t.includes('webhook');\n};\n\nconst getSimpleType = (fullType) => {\n  if (!fullType) return 'unknown';\n  const parts = fullType.split('.');\n  return parts[parts.length - 1] || fullType;\n};\n\nconst truncate = (str, max) => !str ? '' : str.length <= max ? str : str.substring(0, max) + '...';\n\nconst safeGet = (obj, path, fallback) => {\n  let cur = obj;\n  for (const k of path.split('.')) {\n    if (cur == null) return fallback;\n    cur = cur[k];\n  }\n  return cur !== undefined ? cur : fallback;\n};\n\nfunction getNodeDimensions(node) {\n  const isSubNode = !!subNodeParents[node.id];\n  const isAIParent = !!aiSubNodes[node.id];\n  if (isSubNode) return { width: CONFIG_SIZE[0], height: CONFIG_SIZE[1] };\n  const inputSlots = maxInputIndex[node.name] || 1;\n  const outputSlots = maxOutputIndex[node.name] || 1;\n  const maxHandles = Math.max(inputSlots, outputSlots, 1);\n  const height = DEFAULT_SIZE[1] + Math.max(0, maxHandles - 2) * GRID * 2;\n  if (isAIParent) {\n    const subCount = aiSubNodes[node.id].length;\n    const portCount = Math.max(4, subCount);\n    const calcWidth = 80 + GRID * ((portCount - 1) * 3);\n    return { width: Math.max(CONFIGURABLE_MIN_WIDTH, calcWidth), height };\n  }\n  return { width: DEFAULT_SIZE[0], height };\n}\n\nfunction extractContext(node) {\n  const p = node.parameters || {};\n  const type = getSimpleType(node.type);\n  switch (type) {\n    case 'httpRequest': return { description: `${p.method || 'GET'} request to ${truncate(p.url, 40) || 'URL'}` };\n    case 'set': {\n      const fields = safeGet(p, 'assignments.assignments', []).slice(0, 4).map(a => a.name).filter(Boolean);\n      return { description: `Sets: ${fields.join(', ') || 'fields'}` };\n    }\n    case 'if': return { description: 'Conditional branch' };\n    case 'switch': return { description: 'Multi-way branch' };\n    case 'code': {\n      const first = (p.jsCode || '').split('\\n')[0] || '';\n      const hint = first.trim().startsWith('//') ? first.replace('//', '').trim() : 'Custom code';\n      return { description: hint };\n    }\n    case 'filter': return { description: 'Filters items' };\n    case 'merge': return { description: `Merge: ${p.mode || 'append'}` };\n    case 'agent': return { description: 'AI Agent' };\n    default:\n      if (isTrigger(node)) return { description: `${type} trigger` };\n      return { description: `${type} node` };\n  }\n}\n\nconst nodes = [];\nfor (const node of cleanedNodes) {\n  if (subNodeParents[node.id]) continue;\n  const dims = getNodeDimensions(node);\n  const subs = (aiSubNodes[node.id] || []).map(s => ({\n    id: s.id, name: s.name, type: s.type,\n    position: { x: s.position[0], y: s.position[1] },\n    dimensions: getNodeDimensions({ id: s.id, type: s.type, name: s.name })\n  }));\n  nodes.push({\n    id: node.id, name: node.name,\n    simpleType: getSimpleType(node.type),\n    position: { x: node.position[0], y: node.position[1] },\n    dimensions: dims,\n    context: extractContext(node),\n    connectsTo: outgoing[node.name] || [],\n    connectsFrom: incoming[node.name] || [],\n    isEntryPoint: isTrigger(node) || (incoming[node.name] || []).length === 0,\n    subNodes: subs\n  });\n}\n\nreturn { workflowName: workflow.name || 'Workflow client', nodeCount: nodes.length, nodes, aiSubNodes };"
      },
      "typeVersion": 2
    },
    {
      "id": "b73bc913-6096-4db9-b10c-37399d84e3a1",
      "name": "Calculate Layout and Stickies",
      "type": "n8n-nodes-base.code",
      "position": [
        3696,
        -288
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const GRID = 16;\nconst PADDING_X = 48;\nconst PADDING_BOTTOM = 64;\nconst MIN_PADDING_TOP = 80;\nconst MIN_STICKY_WIDTH = 240;\nconst MIN_STICKY_HEIGHT = 120;\nconst GAP = 32;\nconst MAX_ITERATIONS = 15;\nconst MAIN_STICKY_WIDTH = 520;\nconst MAIN_STICKY_MIN_HEIGHT = 420;\nconst MAIN_STICKY_MAX_HEIGHT = 900;\nconst GAP_MAIN_TO_WORKFLOW = 80;\nconst COLOR_WHITE = 7;\n\nconst aiOutput = $input.item.json.output || $input.item.json;\nconst parseData = $('Prepare and Parse Grid Data').item.json;\nconst { nodes: enrichedNodes, aiSubNodes, workflowName } = parseData;\nconst originalWorkflow = $('Set Workflow Config Variables').item.json.workflow;\n\nfunction snapToGrid(val) { return Math.round(val / GRID) * GRID; }\n\nfunction uuid() {\n  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {\n    const r = Math.random() * 16 | 0;\n    return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);\n  });\n}\n\nconst nodeById = {};\nfor (const n of enrichedNodes) {\n  nodeById[n.id] = n;\n  for (const sub of (n.subNodes || [])) nodeById[sub.id] = sub;\n}\n\nfunction estimateStickyTextHeight(title, description, stickyWidth) {\n  const charsPerLine = Math.max(1, Math.floor(stickyWidth / 9));\n  let height = 24;\n  const titleLines = Math.ceil(`## ${title}`.length / charsPerLine);\n  height += titleLines * 36;\n  if (description && description.trim()) {\n    height += 12 + Math.ceil(description.length / charsPerLine) * 22;\n  }\n  height += 20;\n  return Math.max(MIN_PADDING_TOP, height);\n}\n\nconst groupBounds = [];\nfor (const group of aiOutput.groups) {\n  const allNodeIds = [...group.nodeIds];\n  for (const id of group.nodeIds) {\n    if (aiSubNodes[id]) for (const sub of aiSubNodes[id]) allNodeIds.push(sub.id);\n  }\n  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n  for (const id of allNodeIds) {\n    const node = nodeById[id];\n    if (!node) continue;\n    const x = node.position?.x ?? 0;\n    const y = node.position?.y ?? 0;\n    const w = node.dimensions?.width ?? 96;\n    const h = node.dimensions?.height ?? 96;\n    minX = Math.min(minX, x); minY = Math.min(minY, y);\n    maxX = Math.max(maxX, x + w); maxY = Math.max(maxY, y + h);\n  }\n  if (minX === Infinity) continue;\n  const rawWidth = Math.max(MIN_STICKY_WIDTH, maxX - minX + PADDING_X * 2);\n  const textHeight = estimateStickyTextHeight(group.title || '', '', rawWidth);\n  groupBounds.push({\n    groupId: group.groupId, title: group.title,\n    nodeIds: group.nodeIds,\n    sticky: {\n      x: snapToGrid(minX - PADDING_X),\n      y: snapToGrid(minY - textHeight),\n      width: snapToGrid(rawWidth),\n      height: Math.max(MIN_STICKY_HEIGHT, snapToGrid(maxY - minY + textHeight + PADDING_BOTTOM))\n    },\n    textHeight\n  });\n}\n\nconst nodePositions = {};\nfor (const n of enrichedNodes) {\n  nodePositions[n.id] = { x: n.position.x, y: n.position.y };\n  for (const sub of (n.subNodes || [])) nodePositions[sub.id] = { x: sub.position.x, y: sub.position.y };\n}\n\nconst groups = groupBounds.map(g => ({ ...g, sticky: { ...g.sticky }, shifted: { dx: 0, dy: 0 } }));\n\nfunction overlaps(a, b) {\n  return !(a.x + a.width + GAP <= b.x || b.x + b.width + GAP <= a.x || a.y + a.height + GAP <= b.y || b.y + b.height + GAP <= a.y);\n}\nfunction overlapAmount(a, b) {\n  return {\n    x: Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x)),\n    y: Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y))\n  };\n}\n\nlet iterations = 0, hasOverlap = true;\nwhile (hasOverlap && iterations < MAX_ITERATIONS) {\n  hasOverlap = false; iterations++;\n  for (let i = 0; i < groups.length; i++) {\n    for (let j = i + 1; j < groups.length; j++) {\n      const a = groups[i].sticky, b = groups[j].sticky;\n      if (!overlaps(a, b)) continue;\n      hasOverlap = true;\n      const ov = overlapAmount(a, b);\n      let dx = 0, dy = 0;\n      if (ov.x <= ov.y) { dx = ov.x + GAP; if (b.x + b.width / 2 < a.x + a.width / 2) dx = -dx; }\n      else { dy = ov.y + GAP; if (b.y + b.height / 2 < a.y + a.height / 2) dy = -dy; }\n      groups[j].sticky.x += snapToGrid(dx); groups[j].sticky.y += snapToGrid(dy);\n      groups[j].shifted.dx += snapToGrid(dx); groups[j].shifted.dy += snapToGrid(dy);\n    }\n  }\n}\n\nconst finalPositions = { ...nodePositions };\nfor (const group of groups) {\n  if (group.shifted.dx === 0 && group.shifted.dy === 0) continue;\n  const allIds = [...group.nodeIds];\n  for (const id of group.nodeIds) {\n    if (aiSubNodes[id]) for (const sub of aiSubNodes[id]) allIds.push(sub.id);\n  }\n  for (const id of allIds) {\n    if (!finalPositions[id]) continue;\n    finalPositions[id] = {\n      x: snapToGrid(finalPositions[id].x + group.shifted.dx),\n      y: snapToGrid(finalPositions[id].y + group.shifted.dy)\n    };\n  }\n}\n\nfunction formatMainOverview(overview, name) {\n  let content = `## ${name}\\n\\n### Ce que vous gagnez\\n\\n${overview.howItWorks}\\n\\n`;\n  if (overview.customization?.trim()) content += `### Personnalisation\\n\\n${overview.customization}`;\n  return content;\n}\n\nfunction estimateContentHeight(content, width) {\n  const charsPerLine = Math.floor(width / 8);\n  let height = 60;\n  for (const line of content.split('\\n')) {\n    if (line.startsWith('## ')) height += 40;\n    else if (line.startsWith('### ')) height += 32;\n    else if (line.trim() === '') height += 16;\n    else if (line.startsWith('- ')) height += Math.ceil(line.length / charsPerLine) * 24;\n    else height += Math.ceil(line.length / charsPerLine) * 22;\n  }\n  return height + 60;\n}\n\nlet wfMinX = Infinity, wfMinY = Infinity, wfMaxY = -Infinity;\nfor (const g of groups) {\n  wfMinX = Math.min(wfMinX, g.sticky.x);\n  wfMinY = Math.min(wfMinY, g.sticky.y);\n  wfMaxY = Math.max(wfMaxY, g.sticky.y + g.sticky.height);\n}\nif (wfMinX === Infinity) { wfMinX = 176; wfMinY = 240; wfMaxY = 500; }\n\nconst mainContent = formatMainOverview(aiOutput.mainOverview, workflowName);\nconst mainHeight = Math.max(MAIN_STICKY_MIN_HEIGHT, Math.min(MAIN_STICKY_MAX_HEIGHT,\n  Math.max(estimateContentHeight(mainContent, MAIN_STICKY_WIDTH), wfMaxY - wfMinY)));\n\nconst stickies = [{\n  parameters: { content: mainContent, width: MAIN_STICKY_WIDTH, height: snapToGrid(mainHeight) },\n  type: 'n8n-nodes-base.stickyNote', typeVersion: 1,\n  position: [snapToGrid(wfMinX - MAIN_STICKY_WIDTH - GAP_MAIN_TO_WORKFLOW), snapToGrid(wfMinY)],\n  id: uuid(), name: 'Sticky Note'\n}];\n\nfor (let i = 0; i < groups.length; i++) {\n  const g = groups[i];\n  const content = `## ${g.title}`;\n  stickies.push({\n    parameters: { content, width: snapToGrid(g.sticky.width), height: snapToGrid(g.sticky.height), color: COLOR_WHITE },\n    type: 'n8n-nodes-base.stickyNote', typeVersion: 1,\n    position: [snapToGrid(g.sticky.x), snapToGrid(g.sticky.y)],\n    id: uuid(), name: `Sticky Note${i + 1}`\n  });\n}\n\nconst workflow = JSON.parse(JSON.stringify(originalWorkflow));\nconst nodesWithoutStickies = workflow.nodes.filter(n => n.type !== 'n8n-nodes-base.stickyNote');\nfor (const node of nodesWithoutStickies) {\n  const pos = finalPositions[node.id];\n  if (pos) node.position = [pos.x, pos.y];\n}\nworkflow.nodes = [...stickies, ...nodesWithoutStickies];\n\nreturn { workflow };"
      },
      "typeVersion": 2
    },
    {
      "id": "fc3114e7-1448-4a00-995f-6c0192ebc420",
      "name": "Apply Node Renames",
      "type": "n8n-nodes-base.code",
      "position": [
        4512,
        -288
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const aiOutput = $input.item.json.output || $input.item.json;\nconst originalWorkflow = $('Calculate Layout and Stickies').item.json.workflow;\n\nif (!aiOutput.renames || !Array.isArray(aiOutput.renames)) throw new Error('Sortie IA invalide : tableau renames manquant');\n\nconst workflow = JSON.parse(JSON.stringify(originalWorkflow));\n\nconst idToNewName = {};\nconst oldToNewName = {};\nconst usedNames = new Set();\n\nfor (const rename of aiOutput.renames) {\n  let newName = rename.newName;\n  let baseName = newName;\n  let counter = 1;\n  while (usedNames.has(newName)) { newName = `${baseName} ${counter}`; counter++; }\n  usedNames.add(newName);\n  idToNewName[rename.id] = newName;\n  oldToNewName[rename.currentName] = newName;\n}\n\nfunction escapeRegex(str) { return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'); }\n\nfunction updateExpressionRefs(str, oldToNew) {\n  if (typeof str !== 'string') return str;\n  let result = str;\n  for (const [oldName, newName] of Object.entries(oldToNew)) {\n    const patterns = [new RegExp(`\\\\$\\\\('${escapeRegex(oldName)}'\\\\)`, 'g'), new RegExp(`\\\\$\\\\(\"${escapeRegex(oldName)}\"\\\\)`, 'g')];\n    for (const pattern of patterns) result = result.replace(pattern, `$('${newName}')`);\n  }\n  return result;\n}\n\nfunction updateExpressionsInObject(obj, oldToNew) {\n  if (obj === null || obj === undefined) return obj;\n  if (typeof obj === 'string') return updateExpressionRefs(obj, oldToNew);\n  if (Array.isArray(obj)) return obj.map(item => updateExpressionsInObject(item, oldToNew));\n  if (typeof obj === 'object') {\n    const result = {};\n    for (const [key, value] of Object.entries(obj)) result[key] = updateExpressionsInObject(value, oldToNew);\n    return result;\n  }\n  return obj;\n}\n\nfor (const node of workflow.nodes) {\n  if (node.type === 'n8n-nodes-base.stickyNote') continue;\n  const newName = idToNewName[node.id];\n  if (newName) node.name = newName;\n  if (node.parameters) node.parameters = updateExpressionsInObject(node.parameters, oldToNewName);\n}\n\nconst newConnections = {};\nfor (const [oldSourceName, connDef] of Object.entries(workflow.connections || {})) {\n  const newSourceName = oldToNewName[oldSourceName] || oldSourceName;\n  const newConnDef = JSON.parse(JSON.stringify(connDef));\n  for (const [connType, outputs] of Object.entries(newConnDef)) {\n    if (!Array.isArray(outputs)) continue;\n    for (const outputArr of outputs) {\n      if (!Array.isArray(outputArr)) continue;\n      for (const conn of outputArr) { if (conn && conn.node) conn.node = oldToNewName[conn.node] || conn.node; }\n    }\n  }\n  newConnections[newSourceName] = newConnDef;\n}\nworkflow.connections = newConnections;\n\nif (workflow.pinData) {\n  const newPinData = {};\n  for (const [oldName, data] of Object.entries(workflow.pinData)) newPinData[oldToNewName[oldName] || oldName] = data;\n  workflow.pinData = newPinData;\n}\n\nreturn { json: { workflow } };"
      },
      "typeVersion": 2
    },
    {
      "id": "d0056716-0db5-430e-9e30-69b0c4cc9e8d",
      "name": "Create Workflow via n8n API",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4736,
        -288
      ],
      "parameters": {
        "url": "Your n8n url/api/v1/workflows",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ name: $json.workflow.name, nodes: $json.workflow.nodes, connections: $json.workflow.connections, settings: $json.workflow.settings }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "n8nApi"
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "bd7d49e6-b536-4f88-b313-071e54bd99cd",
      "name": "Workflow Description Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1728,
        -288
      ],
      "parameters": {
        "text": "={{ $json['Call transcribing'] }}",
        "options": {
          "systemMessage": "# Role\n\nYou are an n8n automation expert for SMBs and mid-sized companies. You receive the transcript of a sales meeting with a prospective client.\n\n# Mission\n\nAnalyze this transcript and produce a structured list of n8n workflows to create in order to address the needs identified during the conversation.\n\n# Structure of each workflow\n\nFor EACH distinct workflow, provide the following fields:\n\n* **id**: sequential number starting from 1\n* **titre**: short workflow name, business-oriented (example: \"Market Signal Monitoring & Lead Sourcing\")\n* **description_metier**: what the workflow does in client-friendly language, without technical jargon (2\u20133 sentences)\n* **outils_mentionnes**: list of tools, APIs, and services mentioned in the transcript that relate to this workflow\n* **logique_conditionnelle**: describe the required if/then branches, or \"None\" if the flow is linear\n* **note_client**: ONE sentence summarizing the business need addressed, written in non-technical client language. This sentence will be prominently displayed on the workflow shown to the client.\n* **brief_complet**: detailed step-by-step description in business pseudo-code. Minimum 6 steps. Format: Trigger \u2192 Step 1 [tool] \u2192 Step 2 [tool] \u2192 ... \u2192 Output\n\n# Rules\n\n* Identify needs even if they are only implicitly mentioned in the conversation\n* One workflow per major distinct process\n* If there are variations by region/entity \u2192 ONE configurable workflow, not multiple copies\n* Ignore off-topic sections (small talk, call technical issues, side conversations)\n* Respond ONLY with this JSON, without markdown or tags, without introduction or conclusion\n\n# Expected output format\n\n{\n\"client\": \"Client Name\",\n\"date_audit\": \"2026-01-01\",\n\"workflows\": [\n{\n\"id\": 1,\n\"titre\": \"Workflow Title\",\n\"description_metier\": \"Client-friendly description.\",\n\"outils_mentionnes\": [\"Tool 1\", \"Tool 2\"],\n\"logique_conditionnelle\": \"None\",\n\"note_client\": \"One sentence summarizing the business need.\",\n\"brief_complet\": \"Trigger \u2192 Step 1 [Tool] \u2192 Step 2 [Tool] \u2192 Output\"\n}\n]\n}\n"
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3.1
    },
    {
      "id": "db964c76-64b3-4ae7-8191-6f373e4f17f3",
      "name": "Claude Model for Description",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        1728,
        -96
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": {}
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "1e92a523-a38e-4f91-a91d-1cd37253ec43",
      "name": "Description Structured Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1872,
        -96
      ],
      "parameters": {
        "jsonSchemaExample": "{\n  \"client\": \"Nom du client\",\n  \"date_audit\": \"2026-01-01\",\n  \"workflows\": [\n    {\n      \"id\": 1,\n      \"titre\": \"Titre du workflow\",\n      \"description_metier\": \"Description en langage client.\",\n      \"outils_mentionnes\": [\"Outil 1\", \"Outil 2\"],\n      \"logique_conditionnelle\": \"Aucun\",\n      \"note_client\": \"Une phrase r\u00e9sumant le besoin business.\",\n      \"brief_complet\": \"D\u00e9clencheur \u2192 \u00c9tape 1 [Outil] \u2192 \u00c9tape 2 [Outil] \u2192 Sortie\"\n    }\n  ]\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "a57ca0e1-7214-48a4-bd13-d04c89cecc27",
      "name": "Parse JSON Agent Output",
      "type": "n8n-nodes-base.code",
      "position": [
        2080,
        -288
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\n// R\u00e9cup\u00e8re l'output de l'agent IA\nconst output = input.output;\n\nif (!output || !output.workflows || !Array.isArray(output.workflows)) {\n  throw new Error('Structure invalide : output.workflows manquant ou non-array');\n}\n\nreturn output.workflows.map(wf => ({\n  json: {\n    client: output.client || 'Client',\n    date_audit: output.date_audit || null,\n    workflow_id: wf.id,\n    titre: wf.titre,\n    description_metier: wf.description_metier,\n    outils_mentionnes: Array.isArray(wf.outils_mentionnes) ? wf.outils_mentionnes : [],\n    logique_conditionnelle: wf.logique_conditionnelle || 'Aucun',\n    note_client: wf.note_client || '',\n    brief_complet: wf.brief_complet || ''\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "87ad7b2d-a819-4636-8676-a5fec194793d",
      "name": "When Form Submitted",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        1504,
        -288
      ],
      "parameters": {
        "options": {},
        "formTitle": "Commercial call transcribing to n8n workflow",
        "formFields": {
          "values": [
            {
              "fieldType": "textarea",
              "fieldLabel": "Call transcribing"
            }
          ]
        },
        "formDescription": "Paste your sales call transcribing here to generate a workflow design for your outreach sequence."
      },
      "typeVersion": 2.5
    }
  ],
  "connections": {
    "Apply Node Renames": {
      "main": [
        [
          {
            "node": "Create Workflow via n8n API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Node Renaming Agent": {
      "main": [
        [
          {
            "node": "Apply Node Renames",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Form Submitted": {
      "main": [
        [
          {
            "node": "Workflow Description Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Logical Groups Agent": {
      "main": [
        [
          {
            "node": "Calculate Layout and Stickies",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Model for Groups": {
      "ai_languageModel": [
        [
          {
            "node": "Logical Groups Agent",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Groups Structured Parser",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Parse JSON Agent Output": {
      "main": [
        [
          {
            "node": "Workflow Generator Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse and Validate JSON": {
      "main": [
        [
          {
            "node": "Set Workflow Config Variables",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Groups Structured Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Logical Groups Agent",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Parse Nodes for Renaming": {
      "main": [
        [
          {
            "node": "Node Renaming Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Workflow Generator Agent": {
      "main": [
        [
          {
            "node": "Parse and Validate JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Model for Renaming": {
      "ai_languageModel": [
        [
          {
            "node": "Node Renaming Agent",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Renaming Structured Parser",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Claude Model for Generator": {
      "ai_languageModel": [
        [
          {
            "node": "Workflow Generator Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Renaming Structured Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Node Renaming Agent",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Workflow Description Agent": {
      "main": [
        [
          {
            "node": "Parse JSON Agent Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare and Parse Grid Data": {
      "main": [
        [
          {
            "node": "Logical Groups Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Model for Description": {
      "ai_languageModel": [
        [
          {
            "node": "Workflow Description Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Layout and Stickies": {
      "main": [
        [
          {
            "node": "Parse Nodes for Renaming",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Description Structured Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Workflow Description Agent",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Set Workflow Config Variables": {
      "main": [
        [
          {
            "node": "Prepare and Parse Grid Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}