{"workflow":{"id":13868,"name":"Auto-generate sticky notes and rename nodes","views":754,"recentViews":5,"totalViews":754,"createdAt":"2026-03-04T16:13:13.153Z","description":"This is an official n8n workflow that helps you follow our [sticky note and naming guidelines](https://n8n.notion.site/Sticky-note-guidelines-for-templates-2aa5b6e0c94f8058b0aefddd02655887) - required for getting your template published on the n8n template library.\n\n**How it works:**\n\n1. Parses the workflow's nodes, connections, and spatial layout.\n2. Uses GPT-4o to group nodes into logical clusters and generate descriptive sticky notes.\n3. Resolves any overlapping sticky notes through iterative collision detection.\n4. Optionally renames all nodes to follow descriptive naming conventions via a second AI pass.\n\n## Setup steps\n\n- Add your OpenAI API credentials to the two OpenAI Chat Model nodes.\n- Paste your target workflow JSON into the \"Set Workflow Variables\" node.\n- Set `renameNodes` to `true` or `false` depending on whether you want node renaming.","workflow":{"meta":{"instanceId":"84ba6d895254e080ac2b4916d987aa66b000f88d4d919a6b9c76848f9b8a7616"},"nodes":[{"id":"9ac0258b-4280-4e51-92b9-73bdb68a9fda","name":"Sticky Note1","type":"n8n-nodes-base.stickyNote","position":[1664,224],"parameters":{"color":7,"width":368,"height":272,"content":"## Initialize workflow\n\nStarts the workflow and sets initial variables."},"typeVersion":1},{"id":"9b2ebac0-c8d3-46b4-81a1-0a9ba16797e1","name":"Sticky Note2","type":"n8n-nodes-base.stickyNote","position":[2064,224],"parameters":{"color":7,"width":368,"height":272,"content":"## Prepare and parse nodes\n\nPrepares nodes and parses them for further processing."},"typeVersion":1},{"id":"d406950c-0fb6-431e-ae54-733c742558a5","name":"Sticky Note3","type":"n8n-nodes-base.stickyNote","position":[2464,224],"parameters":{"color":7,"width":448,"height":496,"content":"## AI logical grouping\n\nUses AI to logically group nodes."},"typeVersion":1},{"id":"357b6500-6a50-4881-9e0c-c19033e0431f","name":"Sticky Note4","type":"n8n-nodes-base.stickyNote","position":[2944,224],"parameters":{"color":7,"width":1072,"height":256,"content":"## Collision handling and export\n\nHandles collisions and merges results for export."},"typeVersion":1},{"id":"da1723ee-9f3e-468e-8c9a-a6d250ff328f","name":"Sticky Note5","type":"n8n-nodes-base.stickyNote","position":[1664,1008],"parameters":{"color":7,"width":592,"height":320,"content":"## Iterative adjustment\n\nControls loop for iterative collision fixes and picks the best result."},"typeVersion":1},{"id":"e8557f66-051d-4fb7-8341-672215412b45","name":"Sticky Note6","type":"n8n-nodes-base.stickyNote","position":[2288,752],"parameters":{"color":7,"width":800,"height":576,"content":"## Conditional node renaming\n\nDecides and executes the renaming procedure with AI assistance."},"typeVersion":1},{"id":"5c2f420e-733b-4fdb-83f2-c5ebbca8a21d","name":"Sticky Note7","type":"n8n-nodes-base.stickyNote","position":[3120,752],"parameters":{"color":7,"width":832,"height":576,"content":"## Final output preparation\n\nFormats and normalizes the final workflow for output."},"typeVersion":1},{"id":"041f5632-69b8-4e1f-ae72-bc9431c49b8b","name":"Start","type":"n8n-nodes-base.manualTrigger","position":[1696,352],"parameters":{},"typeVersion":1},{"id":"f7e1e2d2-76cc-4ab5-9c48-f3e86f0ea063","name":"Parse Nodes","type":"n8n-nodes-base.code","position":[2288,352],"parameters":{"jsCode":"const GRID = 16;\nconst DEFAULT_SIZE = [96, 96];\nconst CONFIG_SIZE = [80, 80];\nconst CONFIGURABLE_MIN_WIDTH = 256;\n\nconst data = $input.first().json;\nconst {\n  cleanedNodes, aiSubNodes, subNodeParents,\n  outgoing, incoming, maxInputIndex, maxOutputIndex, workflowName\n} = data;\n\n// --- Helpers ---\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) =>\n  !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\n// --- Node dimension calculator ---\n// Based on n8n source: packages/frontend/editor-ui/src/app/utils/nodeViewUtils.ts\n\nfunction getNodeDimensions(node) {\n  const isSubNode = !!subNodeParents[node.id];\n  const isAIParent = !!aiSubNodes[node.id];\n\n  // Configuration nodes (sub-nodes) are circular 80x80\n  if (isSubNode) {\n    return { width: CONFIG_SIZE[0], height: CONFIG_SIZE[1] };\n  }\n\n  // Calculate height from actual input/output SLOT count (not connection count)\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\n  // Configurable nodes (AI parents) are wider\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\n  // Default nodes\n  return { width: DEFAULT_SIZE[0], height };\n}\n\n// --- Context extractor (for AI prompt) ---\n\nfunction extractContext(node) {\n  const p = node.parameters || {};\n  const type = getSimpleType(node.type);\n\n  switch (type) {\n    case 'httpRequest':\n      return { description: `${p.method || 'GET'} request to ${truncate(p.url, 40) || 'URL'}` };\n    case 'set':\n      const fields = safeGet(p, 'assignments.assignments', [])\n        .slice(0, 4).map(a => a.name).filter(Boolean);\n      return { description: `Sets: ${fields.join(', ') || 'fields'}` };\n    case 'if':\n      return { description: 'Conditional branch' };\n    case 'switch':\n      return { description: 'Multi-way branch' };\n    case 'code': {\n      const first = (p.jsCode || '').split('\\n')[0] || '';\n      const hint = first.trim().startsWith('//')\n        ? first.replace('//', '').trim()\n        : 'Custom code';\n      return { description: hint };\n    }\n    case 'filter':\n      return { description: 'Filters items' };\n    case 'merge':\n      return { description: `Merge: ${p.mode || 'append'}` };\n    case 'executeWorkflow':\n      return { description: `Sub-workflow: ${truncate(safeGet(p, 'workflowId.cachedResultName', ''), 30) || 'workflow'}` };\n    case 'postgres':\n      return { description: p.operation === 'executeQuery' ? 'SQL query' : `Postgres ${p.operation || 'op'}` };\n    case 'agent':\n      return { description: 'AI Agent' };\n    case 'chainLlm':\n      return { description: 'LLM Chain' };\n    case 'noOp':\n      return { description: 'No operation (pass-through)' };\n    case 'stopAndError':\n      return { description: 'Stop with error' };\n    case 'executionData':\n      return { description: 'Set execution data' };\n    default:\n      if (isTrigger(node)) return { description: `${type} trigger` };\n      return { description: `${type} node` };\n  }\n}\n\n// --- Build enriched node list (excluding sub-nodes) ---\n\nconst nodes = [];\n\nfor (const node of cleanedNodes) {\n  if (subNodeParents[node.id]) continue;\n\n  const dims = getNodeDimensions(node);\n  const subs = (aiSubNodes[node.id] || []).map(s => ({\n    id: s.id,\n    name: s.name,\n    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\n  nodes.push({\n    id: node.id,\n    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 {\n  json: {\n    workflowName,\n    nodeCount: nodes.length,\n    nodes,\n    aiSubNodes\n  }\n};"},"typeVersion":2},{"id":"4fed7ea4-6f87-477d-9051-41cfc8c459bd","name":"AI Groups Logically","type":"@n8n/n8n-nodes-langchain.agent","position":[2512,336],"parameters":{"text":"=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 considering both their purpose and their spatial position on the canvas.\nEach group should correspond to a visually distinct cluster of nodes.\nEvery node ID must appear in exactly one group.","options":{"systemMessage":"=You are analyzing n8n workflows and grouping nodes based on BOTH their logical purpose AND their spatial position on the canvas.\n\n## Your Task\n\n1. Create logical groups of nodes that work together on the same task\n2. Generate a main overview describing the workflow\n3. Title and describe each group briefly\n\n## Critical: Spatial Awareness\n\nNodes 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 any axis), prefer separate groups\n- A group's nodes should form a visible cluster on the canvas\n- When logically different nodes sit within the same spatial cluster, group them together and expand the description to cover everything that cluster does\n\n## Grouping Guidelines\n\n- Aim for at least {{ Math.ceil((Number($json.nodeCount) || 0) / 3) }} groups depending on workflow complexity\n- Every node must belong to exactly ONE group\n- Smaller, spatially tight groups are better than large sprawling ones\n- A single isolated node can be its own group if it's far from others\n\n## Naming\n\n- Use short, descriptive titles (3-6 words)\n- Sentence case\n- Examples: \"Fetch and validate data\", \"Process AI response\", \"Save results to database\"\n\n## Main Overview\n\n- **howItWorks**: A numbered list of 2-6 items (each on a \\n new line) explaining what the workflow does. Third person. One concise sentence per item.\n- **setupSteps**: Actionable setup instructions. Focus on credentials and configuration needed. Each bullet should start with a markdown checkbox - [ ] \n- **customization**: Optional. Only include if there are obvious customization points.\n\n## Output Rules\n\n- Every node ID from the input must appear in exactly one group's nodeIds array\n- Do not invent node IDs — only use IDs provided in the input\n- Order groups by typical execution flow (trigger/input first, output/error last)"},"promptType":"define","hasOutputParser":true},"typeVersion":3},{"id":"f99294c8-4b0e-4be9-b573-3d03953f7163","name":"OpenAI Chat Model","type":"@n8n/n8n-nodes-langchain.lmChatOpenAi","position":[2512,576],"parameters":{"model":{"__rl":true,"mode":"list","value":"gpt-4o","cachedResultName":"gpt-4o"},"options":{}},"credentials":{"openAiApi":{"id":"QGfM1kqQCGAKKDIo","name":"Template Reviews"}},"typeVersion":1.2},{"id":"0170da4a-2a88-422d-ae09-78021ee7f132","name":"Structured Output Parser","type":"@n8n/n8n-nodes-langchain.outputParserStructured","position":[2656,576],"parameters":{"autoFix":true,"schemaType":"manual","inputSchema":"{\n  \"type\": \"object\",\n  \"properties\": {\n    \"mainOverview\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"howItWorks\": {\n          \"type\": \"string\",\n          \"description\": \"A list of 2-6 items explaining what the workflow does.\"\n        },\n        \"setupSteps\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\" },\n          \"description\": \"Setup instructions\"\n        },\n        \"customization\": {\n          \"type\": \"string\",\n          \"description\": \"Optional customization tips\"\n        }\n      },\n      \"required\": [\"howItWorks\", \"setupSteps\"],\n      \"additionalProperties\": false\n    },\n    \"groups\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"groupId\": {\n            \"type\": \"integer\",\n            \"description\": \"Sequential group ID starting from 0\"\n          },\n          \"title\": {\n            \"type\": \"string\",\n            \"description\": \"Short title, 3-6 words\"\n          },\n          \"description\": {\n            \"type\": \"string\",\n            \"description\": \"Brief description or empty string\"\n          },\n          \"nodeIds\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"string\" },\n            \"description\": \"Array of node IDs in this group\"\n          }\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":"9de1a64a-97c2-40ce-9670-fc52644ca375","name":"Generate Stickies","type":"n8n-nodes-base.code","position":[3472,336],"parameters":{"jsCode":"const GRID = 16;\nconst MAIN_STICKY_WIDTH = 480;\nconst MAIN_STICKY_MIN_HEIGHT = 420;\nconst MAIN_STICKY_MAX_HEIGHT = 900;\nconst GAP_MAIN_TO_WORKFLOW = 80;  // 5 * GRID\nconst COLOR_WHITE = 7;\n\nconst data = $input.first().json;\nconst { groups, finalPositions, mainOverview, aiSubNodes } = data;\nconst parseData = $('Parse Nodes').first().json;\nconst workflowName = parseData.workflowName || 'Workflow overview';\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\nfunction snapToGrid(val) {\n  return Math.round(val / GRID) * GRID;\n}\n\n// --- Build main overview content ---\n\nfunction formatMainOverview(overview, name) {\n  let content = `## ${name}\\n\\n`;\n  content += `### How it works\\n\\n${overview.howItWorks}\\n\\n`;\n  content += `### Setup steps\\n\\n`;\n  for (const step of (overview.setupSteps || [])) {\n    content += `- ${step}\\n`;\n  }\n  if (overview.customization?.trim()) {\n    content += `\\n### Customization\\n\\n${overview.customization}`;\n  }\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('- ') || line.startsWith('* '))\n      height += Math.ceil(line.length / charsPerLine) * 24;\n    else height += Math.ceil(line.length / charsPerLine) * 22;\n  }\n  return height + 60;\n}\n\n// --- Find overall workflow bounds (from final positions of stickies) ---\n\nlet workflowMinX = Infinity;\nlet workflowMinY = Infinity;\nlet workflowMaxY = -Infinity;\n\nfor (const group of groups) {\n  const s = group.sticky;\n  workflowMinX = Math.min(workflowMinX, s.x);\n  workflowMinY = Math.min(workflowMinY, s.y);\n  workflowMaxY = Math.max(workflowMaxY, s.y + s.height);\n}\n\nif (workflowMinX === Infinity) {\n  workflowMinX = 176;\n  workflowMinY = 240;\n  workflowMaxY = 500;\n}\n\n// --- Main overview sticky ---\n\nconst mainContent = formatMainOverview(mainOverview, workflowName);\nconst calcHeight = estimateContentHeight(mainContent, MAIN_STICKY_WIDTH);\nconst workflowSpan = workflowMaxY - workflowMinY;\nconst mainHeight = Math.max(\n  MAIN_STICKY_MIN_HEIGHT,\n  Math.min(MAIN_STICKY_MAX_HEIGHT, Math.max(calcHeight, workflowSpan))\n);\n\nconst stickies = [];\n\nstickies.push({\n  parameters: {\n    content: mainContent,\n    width: MAIN_STICKY_WIDTH,\n    height: snapToGrid(mainHeight)\n  },\n  type: 'n8n-nodes-base.stickyNote',\n  typeVersion: 1,\n  position: [\n    snapToGrid(workflowMinX - MAIN_STICKY_WIDTH - GAP_MAIN_TO_WORKFLOW),\n    snapToGrid(workflowMinY)\n  ],\n  id: uuid(),\n  name: 'Sticky Note'\n});\n\n// --- Group stickies ---\n\nfor (let i = 0; i < groups.length; i++) {\n  const group = groups[i];\n  const s = group.sticky;\n\n  let content = `## ${group.title}`;\n  if (group.description?.trim()) {\n    content += `\\n\\n${group.description}`;\n  }\n\n  stickies.push({\n    parameters: {\n      content,\n      width: snapToGrid(s.width),\n      height: snapToGrid(s.height),\n      color: COLOR_WHITE\n    },\n    type: 'n8n-nodes-base.stickyNote',\n    typeVersion: 1,\n    position: [snapToGrid(s.x), snapToGrid(s.y)],\n    id: uuid(),\n    name: `Sticky Note${i + 1}`\n  });\n}\n\nreturn {\n  json: {\n    stickies,\n    finalPositions,\n    aiSubNodes\n  }\n};"},"typeVersion":2},{"id":"0350bd4c-4f57-428a-8221-f545a0aabfc6","name":"Strip & Prepare","type":"n8n-nodes-base.code","position":[2112,352],"parameters":{"jsCode":"const workflow = $input.first().json.workflow;\nconst allNodes = workflow.nodes || [];\nconst connections = workflow.connections || {};\n\n// 1. Remove existing sticky notes\nconst cleanedNodes = allNodes.filter(\n  n => n.type !== 'n8n-nodes-base.stickyNote'\n);\n\n// 2. Build AI sub-node relationships\n// Sub-nodes connect via ai_languageModel, ai_tool, ai_memory, ai_outputParser\nconst AI_CONNECTION_TYPES = new Set([\n  'ai_languageModel', 'ai_tool', 'ai_memory', 'ai_outputParser'\n]);\n\nconst nodesByName = {};\nfor (const node of cleanedNodes) {\n  nodesByName[node.name] = node;\n}\n\nconst aiSubNodes = {};      // parentId -> [{ id, name, type, position }]\nconst subNodeParents = {};  // subNodeId -> parentId\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\n        if (!aiSubNodes[parentNode.id]) aiSubNodes[parentNode.id] = [];\n        aiSubNodes[parentNode.id].push({\n          id: subNode.id,\n          name: subNode.name,\n          type: subNode.type,\n          position: subNode.position\n        });\n        subNodeParents[subNode.id] = parentNode.id;\n      }\n    }\n  }\n}\n\n// 3. Build main connection maps + count actual input/output SLOTS\nconst outgoing = {};  // nodeName -> [targetNodeNames]\nconst incoming = {};  // nodeName -> [sourceNodeNames]\n\n// Track the highest input/output INDEX used per node (not just count of connections)\nconst maxInputIndex = {};   // nodeName -> highest input index seen\nconst maxOutputIndex = {};  // nodeName -> highest output index (= number of output arrays)\n\nfor (const [fromName, connDef] of Object.entries(connections)) {\n  if (!connDef?.main) continue;\n\n  // The number of output slots = length of the main array\n  maxOutputIndex[fromName] = Math.max(\n    maxOutputIndex[fromName] || 0,\n    connDef.main.length\n  );\n\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\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\n      // Track the input index this connection targets\n      const inputIdx = (conn.index ?? 0) + 1; // index is 0-based, we want count\n      maxInputIndex[conn.node] = Math.max(\n        maxInputIndex[conn.node] || 0,\n        inputIdx\n      );\n    }\n  }\n}\n\nreturn {\n  json: {\n    cleanedNodes,\n    connections,\n    aiSubNodes,\n    subNodeParents,\n    outgoing,\n    incoming,\n    maxInputIndex,\n    maxOutputIndex,\n    workflowName: workflow.name || 'Untitled workflow'\n  }\n};"},"typeVersion":2},{"id":"9004966d-ce52-424b-9593-5c598dc128c1","name":"Compute Bounding Boxes","type":"n8n-nodes-base.code","position":[2992,336],"parameters":{"jsCode":"const GRID = 16;\nconst PADDING_X = 48;          // 3 * GRID — side padding\nconst PADDING_BOTTOM = 64;     // 4 * GRID — matches n8n's STICKY_BOTTOM_PADDING\nconst MIN_PADDING_TOP = 80;    // minimum top padding even for short titles\nconst MIN_STICKY_WIDTH = 240;\nconst MIN_STICKY_HEIGHT = 180;\n\nconst aiOutput = $input.first().json.output || $input.first().json;\nconst parseData = $('Parse Nodes').first().json;\n\nconst { nodes: enrichedNodes, aiSubNodes } = parseData;\n\n// Build lookup: nodeId -> enriched node data\nconst nodeById = {};\nfor (const n of enrichedNodes) {\n  nodeById[n.id] = n;\n  for (const sub of (n.subNodes || [])) {\n    nodeById[sub.id] = sub;\n  }\n}\n\nfunction snapToGrid(val) {\n  return Math.round(val / GRID) * GRID;\n}\n\n// Calculate how much vertical space the sticky text will need\n// so nodes don't overlap with the title/description\nfunction estimateStickyTextHeight(title, description, stickyWidth) {\n  const charsPerLine = Math.max(1, Math.floor(stickyWidth / 9));\n  let height = 24; // top margin inside sticky\n\n  // Title: \"## 1. Title text\"\n  const titleText = `## ${title}`;\n  const titleLines = Math.ceil(titleText.length / charsPerLine);\n  height += titleLines * 36; // h2 line height\n\n  // Description\n  if (description && description.trim()) {\n    height += 12; // gap between title and description\n    const descLines = Math.ceil(description.length / charsPerLine);\n    height += descLines * 22;\n  }\n\n  height += 20; // bottom margin before nodes start\n  return Math.max(MIN_PADDING_TOP, height);\n}\n\n// For each group, compute the bounding box around all member nodes + sub-nodes\nconst groupBounds = [];\n\nfor (const group of aiOutput.groups) {\n  const allNodeIds = [];\n\n  for (const id of group.nodeIds) {\n    allNodeIds.push(id);\n    // Include sub-nodes of any AI parent in this group\n    if (aiSubNodes[id]) {\n      for (const sub of aiSubNodes[id]) {\n        allNodeIds.push(sub.id);\n      }\n    }\n  }\n\n  let minX = Infinity, minY = Infinity;\n  let maxX = -Infinity, maxY = -Infinity;\n\n  for (const id of allNodeIds) {\n    const node = nodeById[id];\n    if (!node) continue;\n\n    const x = node.position?.x ?? node.position?.[0] ?? 0;\n    const y = node.position?.y ?? node.position?.[1] ?? 0;\n    const w = node.dimensions?.width ?? 96;\n    const h = node.dimensions?.height ?? 96;\n\n    minX = Math.min(minX, x);\n    minY = Math.min(minY, y);\n    maxX = Math.max(maxX, x + w);\n    maxY = Math.max(maxY, y + h);\n  }\n\n  if (minX === Infinity) continue; // empty group\n\n  // First pass: estimate sticky width to calculate text height\n  const rawWidth = Math.max(MIN_STICKY_WIDTH, maxX - minX + PADDING_X * 2);\n\n  // Calculate dynamic top padding based on text content\n  const textHeight = estimateStickyTextHeight(\n    group.title || '',\n    group.description || '',\n    rawWidth\n  );\n\n  // Apply padding and snap\n  const stickyX = snapToGrid(minX - PADDING_X);\n  const stickyY = snapToGrid(minY - textHeight);\n  const stickyWidth = snapToGrid(rawWidth);\n  const stickyHeight = Math.max(\n    MIN_STICKY_HEIGHT,\n    snapToGrid(maxY - minY + textHeight + PADDING_BOTTOM)\n  );\n\n  groupBounds.push({\n    groupId: group.groupId,\n    title: group.title,\n    description: group.description,\n    nodeIds: group.nodeIds,\n    sticky: {\n      x: stickyX,\n      y: stickyY,\n      width: stickyWidth,\n      height: stickyHeight\n    },\n    textHeight, // store for reference\n    contentBounds: { minX, minY, maxX, maxY }\n  });\n}\n\nreturn {\n  json: {\n    groupBounds,\n    mainOverview: aiOutput.mainOverview,\n    aiSubNodes\n  }\n};"},"typeVersion":2},{"id":"c2080ff0-77b3-4605-9e57-1570e0aa635f","name":"Collision Resolution","type":"n8n-nodes-base.code","position":[3216,336],"parameters":{"jsCode":"const GRID = 16;\nconst GAP = 32;          // 2 * GRID — minimum gap between stickies\nconst MAX_ITERATIONS = 15;\n\nconst data = $input.first().json;\nconst parseData = $('Parse Nodes').first().json;\n\nconst { groupBounds, mainOverview, aiSubNodes } = data;\nconst { nodes: enrichedNodes } = parseData;\n\nfunction snapToGrid(val) {\n  return Math.round(val / GRID) * GRID;\n}\n\n// Build nodeId -> original position lookup\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 || [])) {\n    nodePositions[sub.id] = { x: sub.position.x, y: sub.position.y };\n  }\n}\n\n// Working copy of group bounds and node positions\nconst groups = groupBounds.map(g => ({\n  ...g,\n  sticky: { ...g.sticky },\n  shifted: { dx: 0, dy: 0 }\n}));\n\n// Detect overlap between two rectangles\nfunction overlaps(a, b) {\n  return !(\n    a.x + a.width + GAP <= b.x ||\n    b.x + b.width + GAP <= a.x ||\n    a.y + a.height + GAP <= b.y ||\n    b.y + b.height + GAP <= a.y\n  );\n}\n\n// Calculate overlap amounts on each axis\nfunction overlapAmount(a, b) {\n  const overX = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);\n  const overY = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);\n  return { x: Math.max(0, overX), y: Math.max(0, overY) };\n}\n\n// Resolve collisions iteratively\n// Groups ordered by index (execution flow) — lower index = higher priority (stays put)\nlet iterations = 0;\nlet hasOverlap = true;\n\nwhile (hasOverlap && iterations < MAX_ITERATIONS) {\n  hasOverlap = false;\n  iterations++;\n\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;\n      const b = groups[j].sticky;\n\n      if (!overlaps(a, b)) continue;\n      hasOverlap = true;\n\n      const overlap = overlapAmount(a, b);\n\n      // Determine shift direction: push along axis of LEAST overlap\n      // (minimum displacement to resolve)\n      let dx = 0, dy = 0;\n\n      if (overlap.x <= overlap.y) {\n        // Push horizontally\n        dx = overlap.x + GAP;\n        // Push right if b is to the right of a, otherwise push left\n        if (b.x + b.width / 2 < a.x + a.width / 2) {\n          dx = -dx;\n        }\n      } else {\n        // Push vertically\n        dy = overlap.y + GAP;\n        if (b.y + b.height / 2 < a.y + a.height / 2) {\n          dy = -dy;\n        }\n      }\n\n      dx = snapToGrid(dx);\n      dy = snapToGrid(dy);\n\n      // Shift group j (lower priority)\n      groups[j].sticky.x += dx;\n      groups[j].sticky.y += dy;\n      groups[j].shifted.dx += dx;\n      groups[j].shifted.dy += dy;\n    }\n  }\n}\n\n// Apply accumulated shifts to node positions\nconst finalPositions = { ...nodePositions };\n\nfor (const group of groups) {\n  if (group.shifted.dx === 0 && group.shifted.dy === 0) continue;\n\n  const allNodeIds = [...group.nodeIds];\n  for (const id of group.nodeIds) {\n    if (aiSubNodes[id]) {\n      for (const sub of aiSubNodes[id]) {\n        allNodeIds.push(sub.id);\n      }\n    }\n  }\n\n  for (const id of allNodeIds) {\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\nreturn {\n  json: {\n    groups,\n    finalPositions,\n    mainOverview,\n    aiSubNodes,\n    totalShifted: groups.filter(g => g.shifted.dx !== 0 || g.shifted.dy !== 0).length,\n    iterations\n  }\n};"},"typeVersion":2},{"id":"c293a6bc-5b01-4a06-9441-40680f88b205","name":"Merge & Export","type":"n8n-nodes-base.code","position":[3680,336],"parameters":{"jsCode":"const stickyData = $input.first().json;\nconst originalWorkflow = $('Set Workflow Variables').first().json.workflow;\nconst { stickies, finalPositions, aiSubNodes } = stickyData;\n\n// Deep copy workflow\nconst workflow = JSON.parse(JSON.stringify(originalWorkflow));\n\n// Remove old stickies\nconst nodesWithoutStickies = workflow.nodes.filter(\n  n => n.type !== 'n8n-nodes-base.stickyNote'\n);\n\n// Update positions for nodes that were shifted during collision resolution\nfor (const node of nodesWithoutStickies) {\n  const pos = finalPositions[node.id];\n  if (pos) {\n    node.position = [pos.x, pos.y];\n  }\n}\n\n// Combine: stickies first (so they render behind nodes), then all nodes\nworkflow.nodes = [...stickies, ...nodesWithoutStickies];\n\nreturn {\n  json: {\n    workflow,\n    workflowJson: JSON.stringify(workflow, null, 2),\n    message: 'Stickies generated — original layout preserved, collisions resolved'\n  }\n};"},"typeVersion":2},{"id":"11105e54-015c-44f9-a991-fd9bad6a09f5","name":"Pick Best Result","type":"n8n-nodes-base.code","position":[2112,1152],"parameters":{"jsCode":"const data = $input.first().json;\nconst workflow = data.bestWorkflow;\n\nreturn {\n  json: {\n    workflow,\n    totalPasses: data.collisionIteration,\n    finalCollisions: data.bestCollisionCount,\n    message: data.bestCollisionCount === 0\n      ? `Clean — no collisions (resolved in ${data.collisionIteration} pass${data.collisionIteration > 1 ? 'es' : ''})`\n      : `Best result after ${data.collisionIteration} passes: ${data.bestCollisionCount} remaining collision${data.bestCollisionCount > 1 ? 's' : ''}`\n  }\n};"},"typeVersion":2},{"id":"4f2d5b38-a856-4cdc-a3d0-a341dbee5223","name":"Collision Detector","type":"n8n-nodes-base.code","position":[3888,336],"parameters":{"jsCode":"const GRID = 16;\nconst GAP = 32;            // minimum gap between stickies\nconst NODE_GAP = 16;       // minimum gap between sticky edge and foreign node\nconst MAX_FIX_PASSES = 10;\n\nconst input = $input.first().json;\nconst workflow = JSON.parse(JSON.stringify(input.workflow));\nconst iteration = (input.collisionIteration || 0) + 1;\n\nfunction snapToGrid(val) {\n  return Math.round(val / GRID) * GRID;\n}\n\n// Separate stickies and regular nodes\nconst stickies = [];\nconst regularNodes = [];\n\nfor (const node of workflow.nodes) {\n  if (node.type === 'n8n-nodes-base.stickyNote') {\n    stickies.push(node);\n  } else {\n    regularNodes.push(node);\n  }\n}\n\n// Get sticky bounds\nfunction stickyRect(s) {\n  return {\n    id: s.id,\n    x: s.position[0],\n    y: s.position[1],\n    w: s.parameters.width || 240,\n    h: s.parameters.height || 180,\n    right() { return this.x + this.w; },\n    bottom() { return this.y + this.h; }\n  };\n}\n\n// Get node bounds (assume 96x96 default — we don't have dimensions here,\n// but this is a safety net, not precision pass)\nfunction nodeRect(n) {\n  return {\n    id: n.id,\n    x: n.position[0],\n    y: n.position[1],\n    w: 96,\n    h: 96,\n    right() { return this.x + this.w; },\n    bottom() { return this.y + this.h; }\n  };\n}\n\n// Check if two rects overlap with a gap\nfunction rectsOverlap(a, b, gap) {\n  return !(\n    a.right() + gap <= b.x ||\n    b.right() + gap <= a.x ||\n    a.bottom() + gap <= b.y ||\n    b.bottom() + gap <= a.y\n  );\n}\n\n// Determine which nodes are \"inside\" each sticky\n// A node is inside a sticky if it's fully contained within the sticky bounds\nfunction nodeInsideSticky(nodeR, stickyR) {\n  return (\n    nodeR.x >= stickyR.x &&\n    nodeR.y >= stickyR.y &&\n    nodeR.right() <= stickyR.right() &&\n    nodeR.bottom() <= stickyR.bottom()\n  );\n}\n\n// Build ownership: which regular nodes belong to which sticky\nconst stickyOwnership = {}; // stickyId -> Set of nodeIds\n\nfor (const s of stickies) {\n  const sr = stickyRect(s);\n  stickyOwnership[s.id] = new Set();\n  for (const n of regularNodes) {\n    const nr = nodeRect(n);\n    if (nodeInsideSticky(nr, sr)) {\n      stickyOwnership[s.id].add(n.id);\n    }\n  }\n}\n\nlet collisionsFound = 0;\n\n// --- Pass 1: Sticky-to-sticky collisions ---\n// Main overview sticky (index 0, no color = yellow) gets highest priority\n// Then ordered by position (left-to-right, top-to-bottom)\n\nconst sortedStickies = [...stickies].sort((a, b) => {\n  // Main overview (no color parameter) gets priority\n  const aIsMain = !a.parameters.color;\n  const bIsMain = !b.parameters.color;\n  if (aIsMain && !bIsMain) return -1;\n  if (!aIsMain && bIsMain) return 1;\n  // Otherwise sort by x position, then y\n  if (a.position[0] !== b.position[0]) return a.position[0] - b.position[0];\n  return a.position[1] - b.position[1];\n});\n\nfor (let pass = 0; pass < MAX_FIX_PASSES; pass++) {\n  let fixedAny = false;\n\n  for (let i = 0; i < sortedStickies.length; i++) {\n    for (let j = i + 1; j < sortedStickies.length; j++) {\n      const a = stickyRect(sortedStickies[i]);\n      const b = stickyRect(sortedStickies[j]);\n\n      if (!rectsOverlap(a, b, GAP)) continue;\n\n      collisionsFound++;\n      fixedAny = true;\n\n      // Calculate overlap on each axis\n      const overX = Math.min(a.right(), b.right()) - Math.max(a.x, b.x) + GAP;\n      const overY = Math.min(a.bottom(), b.bottom()) - Math.max(a.y, b.y) + GAP;\n\n      let dx = 0, dy = 0;\n\n      // Push along axis of least overlap\n      if (overX <= overY) {\n        dx = overX;\n        if (b.x + b.w / 2 < a.x + a.w / 2) dx = -dx;\n      } else {\n        dy = overY;\n        if (b.y + b.h / 2 < a.y + a.h / 2) dy = -dy;\n      }\n\n      dx = snapToGrid(dx);\n      dy = snapToGrid(dy);\n\n      // Shift sticky j\n      sortedStickies[j].position[0] += dx;\n      sortedStickies[j].position[1] += dy;\n\n      // Shift all nodes owned by sticky j\n      const ownedIds = stickyOwnership[sortedStickies[j].id] || new Set();\n      for (const node of regularNodes) {\n        if (ownedIds.has(node.id)) {\n          node.position[0] = snapToGrid(node.position[0] + dx);\n          node.position[1] = snapToGrid(node.position[1] + dy);\n        }\n      }\n    }\n  }\n\n  if (!fixedAny) break;\n}\n\n// --- Pass 2: Check for foreign nodes overlapping stickies ---\n// A \"foreign\" node is one that overlaps a sticky it doesn't belong to\n\nfor (let pass = 0; pass < MAX_FIX_PASSES; pass++) {\n  let fixedAny = false;\n\n  for (const s of stickies) {\n    const sr = stickyRect(s);\n    const owned = stickyOwnership[s.id] || new Set();\n\n    for (const n of regularNodes) {\n      if (owned.has(n.id)) continue; // skip nodes that belong to this sticky\n      const nr = nodeRect(n);\n\n      if (!rectsOverlap(sr, nr, NODE_GAP)) continue;\n\n      // Foreign node overlaps this sticky — expand the sticky to avoid it\n      // OR shift the sticky. Expanding is safer (doesn't cascade).\n      // We'll shrink the sticky edge away from the node.\n\n      // Actually, the safest fix: grow the sticky so its edge clears the node,\n      // but only if the node is just barely clipping. If major overlap,\n      // we shift the sticky instead.\n\n      const overlapArea = (\n        Math.max(0, Math.min(sr.right(), nr.right()) - Math.max(sr.x, nr.x)) *\n        Math.max(0, Math.min(sr.bottom(), nr.bottom()) - Math.max(sr.y, nr.y))\n      );\n\n      // If overlap is small relative to sticky, just note it\n      // The main sticky-to-sticky resolution should have handled most cases\n      if (overlapArea > 0) {\n        collisionsFound++;\n        fixedAny = true;\n\n        // Determine which edge of the sticky the node is closest to\n        const distLeft = Math.abs(nr.right() - sr.x);\n        const distRight = Math.abs(nr.x - sr.right());\n        const distTop = Math.abs(nr.bottom() - sr.y);\n        const distBottom = Math.abs(nr.y - sr.bottom());\n        const minDist = Math.min(distLeft, distRight, distTop, distBottom);\n\n        // Pull the sticky edge back + shift owned nodes\n        const shift = NODE_GAP + GRID;\n        let sdx = 0, sdy = 0;\n\n        if (minDist === distRight) {\n          // Node is near right edge — shrink width\n          s.parameters.width = snapToGrid(Math.max(240, sr.x + sr.w - nr.x - shift));\n        } else if (minDist === distBottom) {\n          // Node is near bottom edge — shrink height\n          s.parameters.height = snapToGrid(Math.max(180, sr.y + sr.h - nr.y - shift));\n        } else if (minDist === distLeft) {\n          // Node is near left edge — shift sticky right\n          sdx = snapToGrid(nr.right() + shift - sr.x);\n          s.position[0] += sdx;\n          s.parameters.width = snapToGrid(Math.max(240, sr.w - sdx));\n        } else {\n          // Node is near top edge — shift sticky down\n          sdy = snapToGrid(nr.bottom() + shift - sr.y);\n          s.position[1] += sdy;\n          s.parameters.height = snapToGrid(Math.max(180, sr.h - sdy));\n        }\n      }\n    }\n  }\n\n  if (!fixedAny) break;\n}\n\n// Reassemble workflow\nworkflow.nodes = [...stickies, ...regularNodes];\n\nreturn {\n  json: {\n    workflow,\n    workflowJson: JSON.stringify(workflow, null, 2),\n    collisionsFound,\n    collisionIteration: iteration,\n    hasCollisions: collisionsFound > 0 && iteration < 5\n  }\n};"},"typeVersion":2},{"id":"2d77d13a-0661-4107-94e7-dee3a596ac29","name":"If","type":"n8n-nodes-base.if","position":[1888,1136],"parameters":{"options":{},"conditions":{"options":{"version":3,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"982b9406-1e75-4f02-b747-bbfae1cb2d88","operator":{"type":"boolean","operation":"true","singleValue":true},"leftValue":"={{ $json._loopRetry }}","rightValue":3}]}},"typeVersion":2.3},{"id":"97fa4a43-26de-443f-896e-e459060ec9b9","name":"Loop Controller (Automatic Fixer)","type":"n8n-nodes-base.code","position":[1712,1136],"parameters":{"jsCode":"const max_retries = $('Set Workflow Variables').first().json.MAX_RETRIES;\nconst state = $getWorkflowStaticData('global');\nconst executionId = $execution.id;\n\nif (state._executionId !== executionId) {\n  state._executionId = executionId;\n  state.loopCount = 0;\n  state.bestCollisionCount = Infinity;\n  state.bestWorkflow = null;\n  state.bestIteration = 0;\n  state.originalWorkflow = $('Set Workflow Variables').first().json.workflow;\n}\n\nconst input = $input.first().json;\nconst currentCollisions = input.collisionsFound;\nconst currentWorkflow = input.workflow;\n\nstate.loopCount += 1;\n\nif (currentCollisions < state.bestCollisionCount) {\n  state.bestCollisionCount = currentCollisions;\n  state.bestWorkflow = JSON.parse(JSON.stringify(currentWorkflow));\n  state.bestIteration = state.loopCount;\n}\n\nconst shouldRetry = currentCollisions > 0 && state.loopCount < max_retries;\n\nif (shouldRetry) {\n  return {\n    json: {\n      workflow: state.originalWorkflow,\n      loopCount: state.loopCount,\n      lastCollisions: currentCollisions,\n      bestSoFar: state.bestCollisionCount,\n      _loopRetry: true\n    }\n  };\n} else {\n  return {\n    json: {\n      bestWorkflow: state.bestWorkflow,\n      bestCollisionCount: state.bestCollisionCount,\n      bestIteration: state.bestIteration,\n      totalPasses: state.loopCount,\n      collisionIteration: state.loopCount,\n      _loopRetry: false\n    }\n  };\n}"},"typeVersion":2},{"id":"1dff0d21-f998-48d1-9894-873317b51993","name":"Set Workflow Variables","type":"n8n-nodes-base.set","position":[1888,352],"parameters":{"options":{},"assignments":{"assignments":[{"id":"453a6d88-c630-4ed7-a0ab-977178b0ef24","name":"workflow","type":"object","value":"={{ $json.workflow }}"},{"id":"a1b55be4-7975-4b80-bc26-0cac271ca761","name":"MAX_RETRIES","type":"number","value":3},{"id":"95aa9bc1-ebc1-433d-9d1c-6d1a64102897","name":"renameNodes","type":"boolean","value":"={{ $json.renameNodes ?? true }}"}]}},"typeVersion":3.4},{"id":"cfaa7dac-121e-4f0d-83a1-8fa1624eab92","name":"Should Rename Nodes","type":"n8n-nodes-base.if","position":[2336,1152],"parameters":{"options":{},"conditions":{"options":{"version":3,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"464093eb-5502-492a-8080-1a997f9f4f83","operator":{"type":"boolean","operation":"true","singleValue":true},"leftValue":"={{ $('Set Workflow Variables').first().json.renameNodes }}","rightValue":""}]}},"typeVersion":2.3},{"id":"4a30959a-66da-42fc-9020-c2f106d81aaf","name":"Output Normalization","type":"n8n-nodes-base.set","position":[3728,1168],"parameters":{"options":{},"assignments":{"assignments":[{"id":"0087aabd-b6a5-4617-94e6-7e4ca1c90cb7","name":"workflow","type":"object","value":"={{ $json.workflow }}"}]}},"typeVersion":3.4},{"id":"128d8236-5bd5-446b-b91f-66d6dd3d2310","name":"Parse for Renaming","type":"n8n-nodes-base.code","position":[2544,864],"parameters":{"jsCode":"// Get the workflow JSON from the first input item\nconst workflow = $input.first().json.workflow;\nconst allNodes = workflow.nodes || [];\nconst connections = workflow.connections || {};\n\n// Helper to detect sticky notes (we'll skip these)\nconst isSticky = (node) => node.type === 'n8n-nodes-base.stickyNote';\n\n// Helper to extract simplified type name\nconst getSimpleType = (fullType) => {\n  if (!fullType) return 'unknown';\n  const parts = fullType.split('.');\n  return parts[parts.length - 1] || fullType;\n};\n\n// Helper to safely get nested value\nconst safeGet = (obj, path, defaultVal = undefined) => {\n  const keys = path.split('.');\n  let current = obj;\n  for (const key of keys) {\n    if (current === null || current === undefined) return defaultVal;\n    current = current[key];\n  }\n  return current !== undefined ? current : defaultVal;\n};\n\n// Helper to truncate string\nconst truncate = (str, maxLen) => {\n  if (!str) return '';\n  if (str.length <= maxLen) return str;\n  return str.substring(0, maxLen) + '...';\n};\n\n// Build connection maps\nconst outgoingConnections = {}; // nodeName -> [targetNames]\nconst incomingConnections = {}; // nodeName -> [sourceNames]\n\nfor (const [fromName, connDef] of Object.entries(connections)) {\n  if (!connDef) continue;\n  \n  // Process main connections\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      \n      // Outgoing from source\n      if (!outgoingConnections[fromName]) outgoingConnections[fromName] = [];\n      outgoingConnections[fromName].push(conn.node);\n      \n      // Incoming to target\n      if (!incomingConnections[conn.node]) incomingConnections[conn.node] = [];\n      incomingConnections[conn.node].push(fromName);\n    }\n  }\n  \n  // Process AI connections (ai_languageModel, ai_tool, etc.)\n  for (const [key, value] of Object.entries(connDef)) {\n    if (key === 'main') continue;\n    if (!Array.isArray(value)) continue;\n    \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        \n        if (!outgoingConnections[fromName]) outgoingConnections[fromName] = [];\n        outgoingConnections[fromName].push(conn.node);\n        \n        if (!incomingConnections[conn.node]) incomingConnections[conn.node] = [];\n        incomingConnections[conn.node].push(fromName);\n      }\n    }\n  }\n}\n\n// Extract context based on node type\nfunction extractContext(node) {\n  const p = node.parameters || {};\n  const type = getSimpleType(node.type);\n  \n  switch (type) {\n    case 'httpRequest':\n      return {\n        method: p.method || 'GET',\n        url: truncate(p.url, 80),\n        authentication: p.authentication || 'none',\n        description: `${p.method || 'GET'} request to ${truncate(p.url, 50) || 'URL'}`\n      };\n      \n    case 'set':\n      const assignments = safeGet(p, 'assignments.assignments', []);\n      const fieldNames = assignments.slice(0, 5).map(a => a.name).filter(Boolean);\n      return {\n        fields: fieldNames,\n        fieldCount: assignments.length,\n        description: `Sets fields: ${fieldNames.join(', ') || 'various'}`\n      };\n      \n    case 'if':\n      const conditions = safeGet(p, 'conditions.conditions', []);\n      const conditionCount = conditions.length;\n      const firstCondition = conditions[0];\n      let conditionDesc = 'condition';\n      if (firstCondition) {\n        const left = String(firstCondition.leftValue || '').substring(0, 30);\n        const op = safeGet(firstCondition, 'operator.operation', 'equals');\n        conditionDesc = `${left} ${op}`;\n      }\n      return {\n        conditionCount,\n        description: `Checks ${conditionDesc}`\n      };\n      \n    case 'switch':\n      const rules = safeGet(p, 'rules.rules', []);\n      return {\n        ruleCount: rules.length,\n        description: `Routes based on ${rules.length} rules`\n      };\n      \n    case 'code':\n      const jsCode = p.jsCode || p.code || '';\n      const firstLine = jsCode.split('\\n')[0] || '';\n      const isComment = firstLine.trim().startsWith('//');\n      return {\n        language: p.language || 'javascript',\n        mode: p.mode || 'runOnceForAllItems',\n        hint: isComment ? firstLine.replace('//', '').trim() : truncate(firstLine, 50),\n        description: isComment ? firstLine.replace('//', '').trim() : 'Custom code'\n      };\n      \n    case 'filter':\n      const filterConditions = safeGet(p, 'conditions.conditions', []);\n      return {\n        conditionCount: filterConditions.length,\n        description: `Filters items based on ${filterConditions.length} condition(s)`\n      };\n      \n    case 'googleSheets':\n      return {\n        operation: p.operation || 'read',\n        sheetName: safeGet(p, 'sheetName.cachedResultName', 'Sheet'),\n        documentName: safeGet(p, 'documentId.cachedResultName', 'Document'),\n        description: `${p.operation || 'Read'} ${safeGet(p, 'sheetName.cachedResultName', 'sheet')}`\n      };\n      \n    case 'slack':\n      return {\n        resource: p.resource || 'message',\n        operation: p.operation || 'post',\n        channel: safeGet(p, 'channelId.cachedResultName', 'channel'),\n        description: `${p.operation || 'Post'} ${p.resource || 'message'} to Slack`\n      };\n      \n    case 'gmail':\n    case 'gmailTrigger':\n      return {\n        operation: p.operation || 'send',\n        description: type === 'gmailTrigger' ? 'Triggered by Gmail' : `${p.operation || 'Send'} email via Gmail`\n      };\n      \n    case 'webhook':\n      return {\n        httpMethod: p.httpMethod || 'GET',\n        path: p.path || '/',\n        description: `Webhook endpoint: ${p.httpMethod || 'GET'} ${p.path || '/'}`\n      };\n      \n    case 'scheduleTrigger':\n      const rule = safeGet(p, 'rule.interval', []);\n      return {\n        rule: rule,\n        description: 'Runs on schedule'\n      };\n      \n    case 'manualTrigger':\n      return {\n        description: 'Manual execution trigger'\n      };\n      \n    case 'wait':\n      return {\n        resumeMode: p.resume || 'timeInterval',\n        amount: p.amount,\n        unit: p.unit,\n        description: p.amount ? `Wait ${p.amount} ${p.unit || 'seconds'}` : 'Wait for event'\n      };\n      \n    case 'splitInBatches':\n      return {\n        batchSize: p.batchSize || 10,\n        description: `Process in batches of ${p.batchSize || 10}`\n      };\n      \n    case 'splitOut':\n      return {\n        fieldToSplit: p.fieldToSplitOut || 'items',\n        description: `Split out ${p.fieldToSplitOut || 'items'}`\n      };\n      \n    case 'merge':\n      return {\n        mode: p.mode || 'append',\n        description: `Merge data: ${p.mode || 'append'}`\n      };\n      \n    case 'removeDuplicates':\n      return {\n        compareField: p.fieldsToCompare || 'all',\n        description: `Remove duplicates by ${p.fieldsToCompare || 'all fields'}`\n      };\n      \n    case 'sort':\n      const sortField = safeGet(p, 'sortFieldsUi.sortField[0].fieldName', 'field');\n      return {\n        sortBy: sortField,\n        description: `Sort by ${sortField}`\n      };\n      \n    case 'limit':\n      return {\n        maxItems: p.maxItems || 10,\n        description: `Limit to ${p.maxItems || 10} items`\n      };\n      \n    case 'agent':\n      return {\n        promptType: p.promptType,\n        description: 'AI Agent node'\n      };\n      \n    case 'lmChatOpenAi':\n      return {\n        model: safeGet(p, 'model.value', 'gpt-4'),\n        description: `OpenAI ${safeGet(p, 'model.value', 'model')}`\n      };\n      \n    case 'lmChatAnthropic':\n      return {\n        model: safeGet(p, 'model.value', 'claude'),\n        description: `Anthropic ${safeGet(p, 'model.value', 'model')}`\n      };\n      \n    default:\n      return {\n        description: `${type} node`\n      };\n  }\n}\n\n// Check if node has expression references to other nodes\nfunction findExpressionReferences(node) {\n  const refs = [];\n  const jsonStr = JSON.stringify(node.parameters || {});\n  \n  // Find $('Node Name') patterns\n  const regex = /\\$\\(['\"]([^'\"]+)['\"]\\)/g;\n  let match;\n  while ((match = regex.exec(jsonStr)) !== null) {\n    refs.push(match[1]);\n  }\n  \n  return refs;\n}\n\n// Process all nodes\nconst nodesForRenaming = [];\nconst existingNames = [];\n\nfor (const node of allNodes) {\n  // Skip sticky notes\n  if (isSticky(node)) continue;\n  \n  existingNames.push(node.name);\n  \n  const context = extractContext(node);\n  const expressionRefs = findExpressionReferences(node);\n  \n  nodesForRenaming.push({\n    id: node.id,\n    currentName: node.name,\n    type: node.type,\n    simpleType: getSimpleType(node.type),\n    context,\n    incomingFrom: [...new Set(incomingConnections[node.name] || [])],\n    outgoingTo: [...new Set(outgoingConnections[node.name] || [])],\n    hasExpressionRefs: expressionRefs.length > 0,\n    expressionRefs\n  });\n}\n\nreturn {\n  json: {\n    workflowName: workflow.name || 'Untitled workflow',\n    nodeCount: nodesForRenaming.length,\n    nodes: nodesForRenaming,\n    existingNames\n  }\n};"},"typeVersion":2},{"id":"5943ae15-d6a1-43f1-ab6a-5026e0c24ed9","name":"AI Rename","type":"@n8n/n8n-nodes-langchain.agent","position":[2784,864],"parameters":{"text":"=Workflow: {{ $json.workflowName }}\nTotal nodes to rename: {{ $json.nodeCount }}\n\nNodes:\n{{ JSON.stringify($json.nodes, null, 2) }}\n\nGenerate a new descriptive name for EACH node listed above. Follow the naming conventions in your instructions.\n\nIMPORTANT: \n- Every node must get a new name\n- All names must be unique\n- Keep names under 40 characters","options":{"systemMessage":"You are renaming nodes in an n8n workflow to make them more descriptive and meaningful.\n\n## Naming Conventions\n\nFollow these patterns based on node type:\n\n| Node Type | Pattern | Examples |\n|-----------|---------|----------|\n| Triggers | \"When [event]\" or \"[Source] Trigger\" | \"When Email Received\", \"Every Morning at 9am\" |\n| HTTP Request | \"[Verb] [what/where]\" | \"Fetch YouTube Videos\", \"Post to Slack API\" |\n| Set | \"[Set/Prepare/Build] [what]\" | \"Prepare Search Queries\", \"Build Output Data\" |\n| If | \"If [condition]\" or \"Check [what]\" | \"If Download Complete\", \"Check Status\" |\n| Switch | \"Route by [criteria]\" | \"Route by Status\", \"Route by Type\" |\n| Code | \"[Verb] [what it does]\" | \"Calculate Duration\", \"Parse API Response\" |\n| Filter | \"Filter [criteria]\" | \"Filter Valid Items\", \"Filter by Date\" |\n| Loop/SplitInBatches | \"Loop Over [items]\" | \"Loop Over Results\", \"Process Each Video\" |\n| Google Sheets | \"[Action] in Sheets\" | \"Append to Sheets\", \"Update Row in Sheets\" |\n| Wait | \"Wait [duration/reason]\" | \"Wait 15 Seconds\", \"Wait for Response\" |\n| Limit | \"Take Top [n]\" or \"Limit to [n]\" | \"Take Top 10 Results\" |\n| Sort | \"Sort by [field]\" | \"Sort by Relevance Score\" |\n| Remove Duplicates | \"Deduplicate by [field]\" | \"Deduplicate by Video ID\" |\n| Split Out | \"Split [what]\" | \"Split Search Results\" |\n| Merge | \"Merge [what]\" | \"Merge All Results\" |\n| AI Agent | \"[Purpose] Agent\" or \"AI [task]\" | \"Content Analyzer Agent\", \"AI Classifier\" |\n| OpenAI/Anthropic | \"OpenAI Model\" or \"Claude Model\" | \"OpenAI GPT-4\", \"Claude Sonnet\" |\n\n## Rules\n\n1. **Be specific**: Use the node's context (URL, fields, conditions) to create meaningful names\n2. **Title Case**: Capitalize each word (except small words like \"to\", \"by\", \"in\", \"for\")\n3. **Under 40 characters**: Keep names concise\n4. **Unique names**: Every name must be different - add specifics if needed to differentiate\n5. **No special characters**: Only letters, numbers, spaces, and hyphens\n6. **Action-oriented**: Start with a verb when possible (Fetch, Send, Check, Filter, etc.)\n\n## Understanding Context\n\n- `context.description`: Summarizes what the node does\n- `context.url`: For HTTP requests, shows the API endpoint\n- `context.fields`: For Set nodes, shows what fields are being set\n- `context.operation`: For service nodes, shows the action (read, append, update)\n- `incomingFrom` / `outgoingTo`: Shows connected nodes for understanding flow\n\n## Examples\n\n| Current Name | Type | Context | Better Name |\n|--------------|------|---------|-------------|\n| HTTP Request | httpRequest | GET youtube.com/search | Fetch YouTube Search Results |\n| Set | set | fields: [query, limit] | Set Search Parameters |\n| If | if | checks status == success | If Request Successful |\n| Code | code | hint: \"Parse duration\" | Parse Video Duration |\n| Google Sheets | googleSheets | operation: append | Append Results to Sheets |\n| Wait | wait | 15 seconds | Wait 15 Seconds |\n| Filter | filter | 2 conditions | Filter Quality Videos |\n\n## Output\n\nReturn a rename for EVERY node. Do not skip any."},"promptType":"define","hasOutputParser":true},"typeVersion":3},{"id":"84610bbc-2ddf-468c-8455-7e83058a00e2","name":"Apply Renames","type":"n8n-nodes-base.code","position":[3168,864],"parameters":{"jsCode":"// Get inputs\nconst aiOutput = $input.first().json.output || $input.first().json;\nconst originalWorkflow = $('Pick Best Result').first().json.workflow;\n\n// Validate AI output\nif (!aiOutput.renames || !Array.isArray(aiOutput.renames)) {\n  throw new Error('Invalid AI output: missing renames array');\n}\n\n// Deep clone the workflow to avoid modifying original\nconst workflow = JSON.parse(JSON.stringify(originalWorkflow));\n\n// Build rename maps\nconst idToNewName = {};      // id -> newName\nconst oldToNewName = {};     // oldName -> newName\nconst usedNames = new Set(); // Track used names to ensure uniqueness\n\n// First pass: collect all renames and ensure uniqueness\nfor (const rename of aiOutput.renames) {\n  let newName = rename.newName;\n  \n  // Ensure uniqueness by adding suffix if needed\n  let baseName = newName;\n  let counter = 1;\n  while (usedNames.has(newName)) {\n    newName = `${baseName} ${counter}`;\n    counter++;\n  }\n  \n  usedNames.add(newName);\n  idToNewName[rename.id] = newName;\n  oldToNewName[rename.currentName] = newName;\n}\n\n// Helper to escape special regex characters\nfunction escapeRegex(str) {\n  return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n// Helper to update expression references in a string\nfunction updateExpressionRefs(str, oldToNew) {\n  if (typeof str !== 'string') return str;\n  \n  let result = str;\n  \n  // Update $('Old Name') to $('New Name')\n  for (const [oldName, newName] of Object.entries(oldToNew)) {\n    // Handle both single and double quotes\n    const patterns = [\n      new RegExp(`\\\\$\\\\('${escapeRegex(oldName)}'\\\\)`, 'g'),\n      new RegExp(`\\\\$\\\\(\"${escapeRegex(oldName)}\"\\\\)`, 'g')\n    ];\n    \n    for (const pattern of patterns) {\n      result = result.replace(pattern, `$('${newName}')`);\n    }\n  }\n  \n  return result;\n}\n\n// Recursively update expression references in an object\nfunction updateExpressionsInObject(obj, oldToNew) {\n  if (obj === null || obj === undefined) return obj;\n  \n  if (typeof obj === 'string') {\n    return updateExpressionRefs(obj, oldToNew);\n  }\n  \n  if (Array.isArray(obj)) {\n    return obj.map(item => updateExpressionsInObject(item, oldToNew));\n  }\n  \n  if (typeof obj === 'object') {\n    const result = {};\n    for (const [key, value] of Object.entries(obj)) {\n      result[key] = updateExpressionsInObject(value, oldToNew);\n    }\n    return result;\n  }\n  \n  return obj;\n}\n\n// ---------- STEP 1: Rename nodes ----------\nfor (const node of workflow.nodes) {\n  // Skip sticky notes\n  if (node.type === 'n8n-nodes-base.stickyNote') continue;\n  \n  const newName = idToNewName[node.id];\n  if (newName) {\n    node.name = newName;\n  }\n  \n  // Update expression references in node parameters\n  if (node.parameters) {\n    node.parameters = updateExpressionsInObject(node.parameters, oldToNewName);\n  }\n}\n\n// ---------- STEP 2: Update connections ----------\n// Connections are keyed by node name and reference target nodes by name\nconst newConnections = {};\n\nfor (const [oldSourceName, connDef] of Object.entries(workflow.connections || {})) {\n  // Get the new source name (or keep old if not renamed)\n  const newSourceName = oldToNewName[oldSourceName] || oldSourceName;\n  \n  // Deep clone the connection definition\n  const newConnDef = JSON.parse(JSON.stringify(connDef));\n  \n  // Update all target node references\n  for (const [connType, outputs] of Object.entries(newConnDef)) {\n    if (!Array.isArray(outputs)) continue;\n    \n    for (const outputArr of outputs) {\n      if (!Array.isArray(outputArr)) continue;\n      \n      for (const conn of outputArr) {\n        if (conn && conn.node) {\n          // Update target node name\n          const newTargetName = oldToNewName[conn.node] || conn.node;\n          conn.node = newTargetName;\n        }\n      }\n    }\n  }\n  \n  newConnections[newSourceName] = newConnDef;\n}\n\nworkflow.connections = newConnections;\n\n// ---------- STEP 3: Update pinData keys if present ----------\nif (workflow.pinData) {\n  const newPinData = {};\n  \n  for (const [oldName, data] of Object.entries(workflow.pinData)) {\n    const newName = oldToNewName[oldName] || oldName;\n    newPinData[newName] = data;\n  }\n  \n  workflow.pinData = newPinData;\n}\n\n// ---------- STEP 4: Build rename report ----------\nconst renameReport = aiOutput.renames.map(r => ({\n  id: r.id,\n  oldName: r.currentName,\n  newName: idToNewName[r.id]\n}));\n\nreturn {\n  json: {\n    workflow,\n    renameReport,\n    totalRenamed: renameReport.length\n  }\n};"},"typeVersion":2},{"id":"8d49a031-392a-4f22-af00-2ec94d7fdc21","name":"Format for Export","type":"n8n-nodes-base.code","position":[3488,864],"parameters":{"jsCode":"// Get the renamed workflow from previous node\nconst data = $input.first().json;\nconst workflow = data.workflow;\nconst renameReport = data.renameReport;\n\n// Return both the workflow JSON string and the rename report\nreturn {\n  json: {\n    // The workflow object for any downstream processing\n    workflow: workflow,\n    \n    // Summary of what was renamed\n    renameReport: renameReport\n  }\n};"},"typeVersion":2},{"id":"769bb4e4-6729-4344-92ba-11aeaf0d9b27","name":"OpenAI Chat Model1","type":"@n8n/n8n-nodes-langchain.lmChatOpenAi","position":[2784,1088],"parameters":{"model":{"__rl":true,"mode":"list","value":"gpt-4o","cachedResultName":"gpt-4o"},"options":{}},"credentials":{"openAiApi":{"id":"QGfM1kqQCGAKKDIo","name":"Template Reviews"}},"typeVersion":1.2},{"id":"5fa4ff96-4ccd-4ce6-9136-3fb91b168e2b","name":"Structured Output Parser1","type":"@n8n/n8n-nodes-langchain.outputParserStructured","position":[2928,1056],"parameters":{"schemaType":"manual","inputSchema":"{\n  \"type\": \"object\",\n  \"properties\": {\n    \"renames\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"string\",\n            \"description\": \"The node ID\"\n          },\n          \"currentName\": {\n            \"type\": \"string\",\n            \"description\": \"The current node name\"\n          },\n          \"newName\": {\n            \"type\": \"string\",\n            \"description\": \"The new descriptive name (under 40 chars, unique)\"\n          }\n        },\n        \"required\": [\"id\", \"currentName\", \"newName\"],\n        \"additionalProperties\": false\n      }\n    }\n  },\n  \"required\": [\"renames\"],\n  \"additionalProperties\": false\n}"},"typeVersion":1.2},{"id":"67b6e6b9-e5a0-4c26-9cca-62659ede7b2a","name":"Sticky Note8","type":"n8n-nodes-base.stickyNote","position":[928,224],"parameters":{"width":624,"height":704,"content":"## Sticky note generator and node renamer\n\n### How it works\nThis workflow creates sticky notes and renames your nodes according to [our guidelines](https://n8n.notion.site/Sticky-note-guidelines-for-templates-2aa5b6e0c94f8058b0aefddd02655887?pvs=74):\n\n@[youtube](RScKsGfrhs4)\n\n### Setup steps\n- [ ] Ensure the 'Set Workflow Variables' node is configured with correct workflow settings.\n- [ ] Set up OpenAI credentials for nodes where AI processing is required.\n\n### Customization\nYou can customize the node processing logic to suit specific workflows or modify AI behavior for different output requirements."},"typeVersion":1},{"id":"0b4c610a-c3d1-4bdd-b9a2-cfdc44cb8b24","name":"Sticky Note","type":"n8n-nodes-base.stickyNote","position":[928,944],"parameters":{"color":4,"width":624,"height":80,"content":"### How can we improve this workflow?\n### [>>> Share your feedback here](https://templates.app.n8n.cloud/form/dbf8d22e-4b05-4328-b164-dcc5555623e0)"},"typeVersion":1}],"pinData":{},"connections":{"If":{"main":[[{"node":"AI Groups Logically","type":"main","index":0}],[{"node":"Pick Best Result","type":"main","index":0}]]},"Start":{"main":[[{"node":"Set Workflow Variables","type":"main","index":0}]]},"AI Rename":{"main":[[{"node":"Apply Renames","type":"main","index":0}]]},"Parse Nodes":{"main":[[{"node":"AI Groups Logically","type":"main","index":0}]]},"Apply Renames":{"main":[[{"node":"Format for Export","type":"main","index":0}]]},"Merge & Export":{"main":[[{"node":"Collision Detector","type":"main","index":0}]]},"Strip & Prepare":{"main":[[{"node":"Parse Nodes","type":"main","index":0}]]},"Pick Best Result":{"main":[[{"node":"Should Rename Nodes","type":"main","index":0}]]},"Format for Export":{"main":[[{"node":"Output Normalization","type":"main","index":0}]]},"Generate Stickies":{"main":[[{"node":"Merge & Export","type":"main","index":0}]]},"OpenAI Chat Model":{"ai_languageModel":[[{"node":"AI Groups Logically","type":"ai_languageModel","index":0},{"node":"Structured Output Parser","type":"ai_languageModel","index":0}]]},"Collision Detector":{"main":[[{"node":"Loop Controller (Automatic Fixer)","type":"main","index":0}]]},"OpenAI Chat Model1":{"ai_languageModel":[[{"node":"AI Rename","type":"ai_languageModel","index":0}]]},"Parse for Renaming":{"main":[[{"node":"AI Rename","type":"main","index":0}]]},"AI Groups Logically":{"main":[[{"node":"Compute Bounding Boxes","type":"main","index":0}]]},"Should Rename Nodes":{"main":[[{"node":"Parse for Renaming","type":"main","index":0}],[{"node":"Output Normalization","type":"main","index":0}]]},"Collision Resolution":{"main":[[{"node":"Generate Stickies","type":"main","index":0}]]},"Compute Bounding Boxes":{"main":[[{"node":"Collision Resolution","type":"main","index":0}]]},"Set Workflow Variables":{"main":[[{"node":"Strip & Prepare","type":"main","index":0}]]},"Structured Output Parser":{"ai_outputParser":[[{"node":"AI Groups Logically","type":"ai_outputParser","index":0}]]},"Structured Output Parser1":{"ai_outputParser":[[{"node":"AI Rename","type":"ai_outputParser","index":0}]]},"Loop Controller (Automatic Fixer)":{"main":[[{"node":"If","type":"main","index":0}]]}}},"lastUpdatedBy":51,"workflowInfo":{"nodeCount":32,"nodeTypes":{"n8n-nodes-base.if":{"count":2},"n8n-nodes-base.set":{"count":2},"n8n-nodes-base.code":{"count":12},"n8n-nodes-base.stickyNote":{"count":9},"n8n-nodes-base.manualTrigger":{"count":1},"@n8n/n8n-nodes-langchain.agent":{"count":2},"@n8n/n8n-nodes-langchain.lmChatOpenAi":{"count":2},"@n8n/n8n-nodes-langchain.outputParserStructured":{"count":2}}},"status":"published","readyToDemo":null,"user":{"name":"Miha","username":"miha","bio":"Template Engineer @ n8n","verified":true,"links":[""],"avatar":"https://gravatar.com/avatar/5625246747ddc04957e7b87ec2ee8d90a9c5622762cbb9f5ac1a35fe24220dde?r=pg&d=retro&size=200"},"nodes":[{"id":20,"icon":"fa:map-signs","name":"n8n-nodes-base.if","codex":{"data":{"alias":["Router","Filter","Condition","Logic","Boolean","Branch"],"details":"The IF node can be used to implement binary conditional logic in your workflow. You can set up one-to-many conditions to evaluate each item of data being inputted into the node. That data will either evaluate to TRUE or FALSE and route out of the node accordingly.\n\nThis node has multiple types of conditions: Bool, String, Number, and Date & Time.","resources":{"generic":[{"url":"https://n8n.io/blog/learn-to-automate-your-factorys-incident-reporting-a-step-by-step-guide/","icon":"🏭","label":"Learn to Automate Your Factory's Incident Reporting: A Step by Step Guide"},{"url":"https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/","icon":"☀️","label":"2021: The Year to Automate the New You with n8n"},{"url":"https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/","icon":"🧬","label":"Why business process automation with n8n can change your daily life"},{"url":"https://n8n.io/blog/create-a-toxic-language-detector-for-telegram/","icon":"🤬","label":"Create a toxic language detector for Telegram in 4 step"},{"url":"https://n8n.io/blog/no-code-ecommerce-workflow-automations/","icon":"store","label":"6 e-commerce workflows to power up your Shopify s"},{"url":"https://n8n.io/blog/how-to-build-a-low-code-self-hosted-url-shortener/","icon":"🔗","label":"How to build a low-code, self-hosted URL shortener in 3 steps"},{"url":"https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/","icon":"⚙️","label":"Automate your data processing pipeline in 9 steps"},{"url":"https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/","icon":"👥","label":"How to get started with CRM automation (with 3 no-code workflow ideas"},{"url":"https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/","icon":"⚡️","label":"5 tasks you can automate with the new Notion API "},{"url":"https://n8n.io/blog/automate-google-apps-for-productivity/","icon":"💡","label":"15 Google apps you can combine and automate to increase productivity"},{"url":"https://n8n.io/blog/automation-for-maintainers-of-open-source-projects/","icon":"🏷️","label":"How to automatically manage contributions to open-source projects"},{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automations for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/","icon":"🧠","label":"Why this Product Manager loves workflow automation with n8n"},{"url":"https://n8n.io/blog/sending-automated-congratulations-with-google-sheets-twilio-and-n8n/","icon":"🙌","label":"Sending Automated Congratulations with Google Sheets, Twilio, and n8n "},{"url":"https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/","icon":"🎡","label":"How to set up a no-code CI/CD pipeline with GitHub and TravisCI"},{"url":"https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/","icon":"🎖","label":"Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin"},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.if/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Flow"]}}},"group":"[\"transform\"]","defaults":{"name":"If","color":"#408000"},"iconData":{"icon":"map-signs","type":"icon"},"displayName":"If","typeVersion":2,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":38,"icon":"fa:pen","name":"n8n-nodes-base.set","codex":{"data":{"alias":["Set","JS","JSON","Filter","Transform","Map"],"resources":{"generic":[{"url":"https://n8n.io/blog/learn-to-automate-your-factorys-incident-reporting-a-step-by-step-guide/","icon":"🏭","label":"Learn to Automate Your Factory's Incident Reporting: A Step by Step Guide"},{"url":"https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/","icon":"☀️","label":"2021: The Year to Automate the New You with n8n"},{"url":"https://n8n.io/blog/automatically-pulling-and-visualizing-data-with-n8n/","icon":"📈","label":"Automatically pulling and visualizing data with n8n"},{"url":"https://n8n.io/blog/database-monitoring-and-alerting-with-n8n/","icon":"📡","label":"Database Monitoring and Alerting with n8n"},{"url":"https://n8n.io/blog/automatically-adding-expense-receipts-to-google-sheets-with-telegram-mindee-twilio-and-n8n/","icon":"🧾","label":"Automatically Adding Expense Receipts to Google Sheets with Telegram, Mindee, Twilio, and n8n"},{"url":"https://n8n.io/blog/no-code-ecommerce-workflow-automations/","icon":"store","label":"6 e-commerce workflows to power up your Shopify s"},{"url":"https://n8n.io/blog/how-to-build-a-low-code-self-hosted-url-shortener/","icon":"🔗","label":"How to build a low-code, self-hosted URL shortener in 3 steps"},{"url":"https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/","icon":"⚙️","label":"Automate your data processing pipeline in 9 steps"},{"url":"https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/","icon":"👥","label":"How to get started with CRM automation (with 3 no-code workflow ideas"},{"url":"https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/","icon":"⚡️","label":"5 tasks you can automate with the new Notion API "},{"url":"https://n8n.io/blog/automate-google-apps-for-productivity/","icon":"💡","label":"15 Google apps you can combine and automate to increase productivity"},{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/building-an-expense-tracking-app-in-10-minutes/","icon":"📱","label":"Building an expense tracking app in 10 minutes"},{"url":"https://n8n.io/blog/the-ultimate-guide-to-automate-your-video-collaboration-with-whereby-mattermost-and-n8n/","icon":"📹","label":"The ultimate guide to automate your video collaboration with Whereby, Mattermost, and n8n"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automations for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/learn-to-build-powerful-api-endpoints-using-webhooks/","icon":"🧰","label":"Learn to Build Powerful API Endpoints Using Webhooks"},{"url":"https://n8n.io/blog/how-a-membership-development-manager-automates-his-work-and-investments/","icon":"📈","label":"How a Membership Development Manager automates his work and investments"},{"url":"https://n8n.io/blog/a-low-code-bitcoin-ticker-built-with-questdb-and-n8n-io/","icon":"📈","label":"A low-code bitcoin ticker built with QuestDB and n8n.io"},{"url":"https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/","icon":"🎡","label":"How to set up a no-code CI/CD pipeline with GitHub and TravisCI"},{"url":"https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/","icon":"🎖","label":"Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin"},{"url":"https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/","icon":"🛵","label":"How Goomer automated their operations with over 200 n8n workflows"},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.set/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Data Transformation"]}}},"group":"[\"input\"]","defaults":{"name":"Edit Fields"},"iconData":{"icon":"pen","type":"icon"},"displayName":"Edit Fields (Set)","typeVersion":3,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":565,"icon":"fa:sticky-note","name":"n8n-nodes-base.stickyNote","codex":{"data":{"alias":["Comments","Notes","Sticky"],"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers"]}}},"group":"[\"input\"]","defaults":{"name":"Sticky Note","color":"#FFD233"},"iconData":{"icon":"sticky-note","type":"icon"},"displayName":"Sticky Note","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":834,"icon":"file:code.svg","name":"n8n-nodes-base.code","codex":{"data":{"alias":["cpde","Javascript","JS","Python","Script","Custom Code","Function"],"details":"The Code node allows you to execute JavaScript in your workflow.","resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/"}]},"categories":["Development","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers","Data Transformation"]}}},"group":"[\"transform\"]","defaults":{"name":"Code"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xMTcxXzQ0MSkiPgo8cGF0aCBkPSJNMTcwLjI4MyA0OEgxOTYuNUMyMDMuMTI3IDQ4IDIwOC41IDQyLjYyNzQgMjA4LjUgMzZWMTJDMjA4LjUgNS4zNzI1OCAyMDMuMTI3IDAgMTk2LjUgMEgxNzAuMjgzQzEyNi4xIDAgOTAuMjgzIDM1LjgxNzIgOTAuMjgzIDgwVjE3NkM5MC4yODMgMjA2LjkyOCA2NS4yMTA5IDIzMiAzNC4yODMgMjMySDIzQzE2LjM3MjYgMjMyIDExIDIzNy4zNzIgMTEgMjQ0VjI2OEMxMSAyNzQuNjI3IDE2LjM3MjQgMjgwIDIyLjk5OTYgMjgwTDM0LjI4MyAyODBDNjUuMjEwOSAyODAgOTAuMjgzIDMwNS4wNzIgOTAuMjgzIDMzNlY0NDBDOTAuMjgzIDQ3OS43NjQgMTIyLjUxOCA1MTIgMTYyLjI4MyA1MTJIMTk2LjVDMjAzLjEyNyA1MTIgMjA4LjUgNTA2LjYyNyAyMDguNSA1MDBWNDc2QzIwOC41IDQ2OS4zNzMgMjAzLjEyNyA0NjQgMTk2LjUgNDY0SDE2Mi4yODNDMTQ5LjAyOCA0NjQgMTM4LjI4MyA0NTMuMjU1IDEzOC4yODMgNDQwVjMzNkMxMzguMjgzIDMwOS4wMjIgMTI4LjAxMSAyODQuNDQzIDExMS4xNjQgMjY1Ljk2MUMxMDYuMTA5IDI2MC40MTYgMTA2LjEwOSAyNTEuNTg0IDExMS4xNjQgMjQ2LjAzOUMxMjguMDExIDIyNy41NTcgMTM4LjI4MyAyMDIuOTc4IDEzOC4yODMgMTc2VjgwQzEzOC4yODMgNjIuMzI2OSAxNTIuNjEgNDggMTcwLjI4MyA0OFoiIGZpbGw9IiNGRjk5MjIiLz4KPHBhdGggZD0iTTMwNSAzNkMzMDUgNDIuNjI3NCAzMTAuMzczIDQ4IDMxNyA0OEgzNDIuOTc5QzM2MC42NTIgNDggMzc0Ljk3OCA2Mi4zMjY5IDM3NC45NzggODBWMTc2QzM3NC45NzggMjAyLjk3OCAzODUuMjUxIDIyNy41NTcgNDAyLjA5OCAyNDYuMDM5QzQwNy4xNTMgMjUxLjU4NCA0MDcuMTUzIDI2MC40MTYgNDAyLjA5OCAyNjUuOTYxQzM4NS4yNTEgMjg0LjQ0MyAzNzQuOTc4IDMwOS4wMjIgMzc0Ljk3OCAzMzZWNDMyQzM3NC45NzggNDQ5LjY3MyAzNjAuNjUyIDQ2NCAzNDIuOTc5IDQ2NEgzMTdDMzEwLjM3MyA0NjQgMzA1IDQ2OS4zNzMgMzA1IDQ3NlY1MDBDMzA1IDUwNi42MjcgMzEwLjM3MyA1MTIgMzE3IDUxMkgzNDIuOTc5QzM4Ny4xNjEgNTEyIDQyMi45NzggNDc2LjE4MyA0MjIuOTc4IDQzMlYzMzZDNDIyLjk3OCAzMDUuMDcyIDQ0OC4wNTEgMjgwIDQ3OC45NzkgMjgwSDQ5MEM0OTYuNjI3IDI4MCA1MDIgMjc0LjYyOCA1MDIgMjY4VjI0NEM1MDIgMjM3LjM3MyA0OTYuNjI4IDIzMiA0OTAgMjMyTDQ3OC45NzkgMjMyQzQ0OC4wNTEgMjMyIDQyMi45NzggMjA2LjkyOCA0MjIuOTc4IDE3NlY4MEM0MjIuOTc4IDM1LjgxNzIgMzg3LjE2MSAwIDM0Mi45NzkgMEgzMTdDMzEwLjM3MyAwIDMwNSA1LjM3MjU4IDMwNSAxMlYzNloiIGZpbGw9IiNGRjk5MjIiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8xMTcxXzQ0MSI+CjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo="},"displayName":"Code","typeVersion":2,"nodeCategories":[{"id":5,"name":"Development"},{"id":9,"name":"Core Nodes"}]},{"id":838,"icon":"fa:mouse-pointer","name":"n8n-nodes-base.manualTrigger","codex":{"data":{"resources":{"generic":[],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.manualworkflowtrigger/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0"}},"group":"[\"trigger\"]","defaults":{"name":"When clicking ‘Execute workflow’","color":"#909298"},"iconData":{"icon":"mouse-pointer","type":"icon"},"displayName":"Manual Trigger","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":1119,"icon":"fa:robot","name":"@n8n/n8n-nodes-langchain.agent","codex":{"data":{"alias":["LangChain","Chat","Conversational","Plan and Execute","ReAct","Tools"],"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Agents","Root Nodes"]}}},"group":"[\"transform\"]","defaults":{"name":"AI Agent","color":"#404040"},"iconData":{"icon":"robot","type":"icon"},"displayName":"AI Agent","typeVersion":3,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1153,"icon":"file:openAiLight.svg","name":"@n8n/n8n-nodes-langchain.lmChatOpenAi","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatopenai/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Language Models","Root Nodes"],"Language Models":["Chat Models (Recommended)"]}}},"group":"[\"transform\"]","defaults":{"name":"OpenAI Chat Model"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTM2Ljg2NzEgMTYuMzcxOEMzNy43NzQ2IDEzLjY0OCAzNy40NjIxIDEwLjY2NDIgMzYuMDEwOCA4LjE4NjYxQzMzLjgyODIgNC4zODY1MyAyOS40NDA3IDIuNDMxNDkgMjUuMTU1NiAzLjM1MTUxQzIzLjI0OTMgMS4yMDM5NiAyMC41MTA1IC0wLjAxNzMxNDggMTcuNjM5MiAwLjAwMDE4NTUzM0MxMy4yNTkxIC0wLjAwOTgxNDY4IDkuMzcyNzMgMi44MTAyNSA4LjAyNTIgNi45Nzc4M0M1LjIxMTM5IDcuNTU0MSAyLjc4MjU4IDkuMzE1MzggMS4zNjEzIDExLjgxMTdDLTAuODM3NDkzIDE1LjYwMTggLTAuMzM2MjMyIDIwLjM3OTQgMi42MDEzMyAyMy42Mjk0QzEuNjkzODEgMjYuMzUzMiAyLjAwNjMyIDI5LjMzNzEgMy40NTc2IDMxLjgxNDZDNS42NDAxNSAzNS42MTQ3IDEwLjAyNzcgMzcuNTY5NyAxNC4zMTI4IDM2LjY0OTdDMTYuMjE3OSAzOC43OTczIDE4Ljk1NzkgNDAuMDE4NSAyMS44MjkyIDM5Ljk5OThDMjYuMjExOCA0MC4wMTEgMzAuMDk5NCAzNy4xODg1IDMxLjQ0NjkgMzMuMDE3MUMzNC4yNjA4IDMyLjQ0MDkgMzYuNjg5NiAzMC42Nzk2IDM4LjExMDggMjguMTgzM0M0MC4zMDcxIDI0LjM5MzIgMzkuODA0NiAxOS42MTk0IDM2Ljg2ODMgMTYuMzY5M0wzNi44NjcxIDE2LjM3MThaTTIxLjgzMTcgMzcuMzg2QzIwLjA3OCAzNy4zODg1IDE4LjM3OTIgMzYuNzc0NyAxNy4wMzI5IDM1LjY1MDlDMTcuMDk0MSAzNS42MTg0IDE3LjIwMDQgMzUuNTU5NyAxNy4yNjkxIDM1LjUxNzJMMjUuMjM0MyAzMC45MTcxQzI1LjY0MTggMzAuNjg1OCAyNS44OTE4IDMwLjI1MjEgMjUuODg5MyAyOS43ODMzVjE4LjU1NDNMMjkuMjU1NyAyMC40OTgxQzI5LjI5MTkgMjAuNTE1NiAyOS4zMTU3IDIwLjU1MDYgMjkuMzIwNyAyMC41OTA2VjI5Ljg4OTZDMjkuMzE1NyAzNC4wMjQ3IDI1Ljk2NjggMzcuMzc3MiAyMS44MzE3IDM3LjM4NlpNNS43MjY0IDMwLjUwNzFDNC44NDc2MyAyOC45ODk2IDQuNTMxMzcgMjcuMjEwOCA0LjgzMjYzIDI1LjQ4NDVDNC44OTEzOCAyNS41MTk1IDQuOTk1MTMgMjUuNTgzMiA1LjA2ODg4IDI1LjYyNTdMMTMuMDM0MSAzMC4yMjU4QzEzLjQzNzggMzAuNDYyMSAxMy45Mzc4IDMwLjQ2MjEgMTQuMzQyOCAzMC4yMjU4TDI0LjA2NjggMjQuNjEwN1YyOC40OTgzQzI0LjA2OTMgMjguNTM4MyAyNC4wNTA1IDI4LjU3NyAyNC4wMTkzIDI4LjYwMkwxNS45Njc5IDMzLjI1MDlDMTIuMzgxNSAzNS4zMTU5IDcuODAxNDQgMzQuMDg4NCA1LjcyNzY1IDMwLjUwNzFINS43MjY0Wk0zLjYzMDEgMTMuMTIwNUM0LjUwNTEyIDExLjYwMDQgNS44ODY0IDEwLjQzNzkgNy41MzE0NCA5LjgzNDE1QzcuNTMxNDQgOS45MDI5IDcuNTI3NjkgMTAuMDI0MiA3LjUyNzY5IDEwLjEwOTJWMTkuMzEwNkM3LjUyNTE5IDE5Ljc3ODEgNy43NzUxOSAyMC4yMTE5IDguMTgxNDUgMjAuNDQzMUwxNy45MDU0IDI2LjA1N0wxNC41MzkxIDI4LjAwMDhDMTQuNTA1MyAyOC4wMjMzIDE0LjQ2MjggMjguMDI3IDE0LjQyNTMgMjguMDEwOEw2LjM3MjY2IDIzLjM1ODJDMi43OTM4MyAyMS4yODU2IDEuNTY2MzEgMTYuNzA2OCAzLjYyODg1IDEzLjEyMTdMMy42MzAxIDEzLjEyMDVaTTMxLjI4ODIgMTkuNTU2OUwyMS41NjQyIDEzLjk0MTdMMjQuOTMwNiAxMS45OTkyQzI0Ljk2NDMgMTEuOTc2NyAyNS4wMDY4IDExLjk3MjkgMjUuMDQ0MyAxMS45ODkyTDMzLjA5NyAxNi42MzhDMzYuNjgyMSAxOC43MDkzIDM3LjkxMDggMjMuMjk1NyAzNS44Mzk1IDI2Ljg4MDhDMzQuOTYzMyAyOC4zOTgzIDMzLjU4MzIgMjkuNTYwOCAzMS45Mzk1IDMwLjE2NThWMjAuNjg5NEMzMS45NDMyIDIwLjIyMTkgMzEuNjk0NSAxOS43ODk0IDMxLjI4OTQgMTkuNTU2OUgzMS4yODgyWk0zNC42MzgzIDE0LjUxNDJDMzQuNTc5NSAxNC40NzggMzQuNDc1OCAxNC40MTU1IDM0LjQwMiAxNC4zNzNMMjYuNDM2OCA5Ljc3Mjg5QzI2LjAzMzEgOS41MzY2NCAyNS41MzMxIDkuNTM2NjQgMjUuMTI4MSA5Ljc3Mjg5TDE1LjQwNDEgMTUuMzg4VjExLjUwMDRDMTUuNDAxNiAxMS40NjA0IDE1LjQyMDQgMTEuNDIxNyAxNS40NTE2IDExLjM5NjdMMjMuNTAzIDYuNzUxNThDMjcuMDg5NCA0LjY4Mjc5IDMxLjY3NDUgNS45MTQwNiAzMy43NDIgOS41MDE2NEMzNC42MTU4IDExLjAxNjcgMzQuOTMyIDEyLjc5MDUgMzQuNjM1OCAxNC41MTQySDM0LjYzODNaTTEzLjU3NDEgMjEuNDQzMUwxMC4yMDY1IDE5LjQ5OTRDMTAuMTcwMiAxOS40ODE5IDEwLjE0NjUgMTkuNDQ2OCAxMC4xNDE1IDE5LjQwNjhWMTAuMTA3OUMxMC4xNDQgNS45Njc4MSAxMy41MDI4IDIuNjEyNzQgMTcuNjQyOSAyLjYxNTI0QzE5LjM5NDIgMi42MTUyNCAyMS4wODkyIDMuMjMwMjUgMjIuNDM1NSA0LjM1MDI4QzIyLjM3NDMgNC4zODI3OCAyMi4yNjkzIDQuNDQxNTMgMjIuMTk5MiA0LjQ4NDAzTDE0LjIzNDEgOS4wODQxM0MxMy44MjY2IDkuMzE1MzggMTMuNTc2NiA5Ljc0Nzg5IDEzLjU3OTEgMTAuMjE2N0wxMy41NzQxIDIxLjQ0MDZWMjEuNDQzMVpNMTUuNDAyOSAxNy41MDA2TDE5LjczNDIgMTQuOTk5M0wyNC4wNjU1IDE3LjQ5OTNWMjIuNTAwN0wxOS43MzQyIDI1LjAwMDdMMTUuNDAyOSAyMi41MDA3VjE3LjUwMDZaIiBmaWxsPSIjN0Q3RDg3Ii8+Cjwvc3ZnPgo="},"displayName":"OpenAI Chat Model","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1179,"icon":"fa:code","name":"@n8n/n8n-nodes-langchain.outputParserStructured","codex":{"data":{"alias":["json","zod"],"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.outputparserstructured/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Output Parsers"]}}},"group":"[\"transform\"]","defaults":{"name":"Structured Output Parser"},"iconData":{"icon":"code","type":"icon"},"displayName":"Structured Output Parser","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]}],"categories":[{"id":30,"name":"Document Ops"},{"id":49,"name":"AI Summarization"}],"image":[]}}