{
  "name": "Analytics Workflow",
  "nodes": [
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "66a3be79-4b84-4503-afeb-f9833f099ddd",
              "name": "csv_data",
              "value": "date,cntl_visitors,cntl_orders,variation_visitors,variation_orders 2026-07-11,10019,1252,10019,1202 2026-07-12,10046,1205,10046,1255 2026-07-13,10030,1203,10088,1261 2026-07-14,10028,300,10028,300 2026-07-15,10010,400,10068,503 2026-07-16,10048,401,10106,505 2026-07-17,10020,400,10078,503 2026-07-18,10048,401,10048,502 2026-07-19,10026,401,10084,504 2026-07-20,10042,1205,10042,1255 2026-07-21,10037,1254,10037,1204 2026-07-22,10046,301,10104,303 2026-07-23,10035,401,10093,504 2026-07-24,10037,1254,10037,1204 2026-07-25,10017,1202,10075,1259 2026-07-26,10027,300,10085,302 2026-07-27,10028,401,10028,501 2026-07-28,10027,300,10086,302 2026-07-29,10015,1201,10073,1259 2026-07-30,10051,402,10109,505 2026-07-31,10011,400,10011,500 2026-08-01,10040,1204,10098,1262 2026-08-02,10031,1203,10089,1261 2026-08-03,10033,1254,10033,1204",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -64,
        240
      ],
      "id": "9055ede7-47b5-4723-a174-5236e2a5a575",
      "name": "Edit Fields3"
    },
    {
      "parameters": {
        "maxItems": 5,
        "keep": "lastItems"
      },
      "type": "n8n-nodes-base.limit",
      "typeVersion": 1,
      "position": [
        240,
        240
      ],
      "id": "392a7623-4384-4c6c-9cdf-1ec6d0ac5993",
      "name": "Limit1"
    },
    {
      "parameters": {
        "model": "qwen/qwen3-coder-next",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        736,
        464
      ],
      "id": "ac64a966-6fc5-4b6e-96e5-554ba600b4e9",
      "name": "OpenRouter Chat Model1",
      "credentials": {
        "openRouterApi": {
          "id": "baaHEuVk7u6Z0Liu",
          "name": "OpenRouter account"
        }
      }
    },
    {
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        416,
        240
      ],
      "id": "9e81755a-2685-46ca-ae64-ea73e5d86964",
      "name": "Aggregate"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        560,
        256
      ],
      "id": "e6bb3a2e-a0c7-47db-9b7e-3e1d3ee26fc8",
      "name": "Merge"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "1e6f246d-d366-40f7-ab46-c557c7267b84",
              "leftValue": "={{ $json.success }}",
              "rightValue": "={{ $json.success }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1472,
        240
      ],
      "id": "5bba0d62-13c5-4c88-a3c9-ab0bbe58106d",
      "name": "If"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -192,
        240
      ],
      "id": "5d17a9e1-1483-4512-8d20-04fe9a791be0",
      "name": "When clicking ‘Execute workflow’"
    },
    {
      "parameters": {
        "operation": "clear",
        "documentId": {
          "__rl": true,
          "value": "1HRWWiJrkOYfRKjqMDlgq6YXEOhbMObtrvYZG2r_6SoU",
          "mode": "list",
          "cachedResultName": "stats output",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1HRWWiJrkOYfRKjqMDlgq6YXEOhbMObtrvYZG2r_6SoU/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "Sheet1",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1HRWWiJrkOYfRKjqMDlgq6YXEOhbMObtrvYZG2r_6SoU/edit#gid=0"
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1664,
        240
      ],
      "id": "360341cc-f984-4908-854e-6e6d5c86ddb9",
      "name": "Clear sheet1",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "dsUvNHMNlqw4axJe",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "865f0814-2b3e-4496-bed3-1bc50be46b62",
              "name": "data",
              "value": "={{ $('API Call').item.json.data }}",
              "type": "array"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1776,
        240
      ],
      "id": "39262334-0ce9-460c-aeb4-10f1c6609ffc",
      "name": "Edit Fields7"
    },
    {
      "parameters": {
        "jsCode": "// Flatten every \"data\" array across all input items into one list of rows\nconst rows = [];\nfor (const item of $input.all()) {\n  rows.push(...(item.json.data || []));\n}\n\n// Dedupe rows that are byte-for-byte identical\nconst seen = new Set();\nconst uniqueRows = rows.filter(row => {\n  const key = JSON.stringify(row);\n  if (seen.has(key)) return false;\n  seen.add(key);\n  return true;\n});\n\n// Column headers = keys of the first unique row\nconst headers = uniqueRows.length ? Object.keys(uniqueRows[0]) : [];\nconst headerRow = { json: Object.fromEntries(headers.map(h => [h, h])) };\n\nconst dataItems = uniqueRows.map(row => ({ json: row }));\n\nreturn [...dataItems];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1888,
        240
      ],
      "id": "049823a1-e15c-4aeb-bb52-1aa3af3731b9",
      "name": "Code1"
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "1HRWWiJrkOYfRKjqMDlgq6YXEOhbMObtrvYZG2r_6SoU",
          "mode": "list",
          "cachedResultName": "stats output",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1HRWWiJrkOYfRKjqMDlgq6YXEOhbMObtrvYZG2r_6SoU/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "Sheet1",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1HRWWiJrkOYfRKjqMDlgq6YXEOhbMObtrvYZG2r_6SoU/edit#gid=0"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [
            "data"
          ],
          "schema": [
            {
              "id": "date",
              "displayName": "date",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "control_visitors",
              "displayName": "control_visitors",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "control_orders",
              "displayName": "control_orders",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "control_conversion",
              "displayName": "control_conversion",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "variation_visitors",
              "displayName": "variation_visitors",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "variation_orders",
              "displayName": "variation_orders",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "variation_conversion",
              "displayName": "variation_conversion",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "conversion_difference",
              "displayName": "conversion_difference",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "probability",
              "displayName": "probability",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "expected_loss_test",
              "displayName": "expected_loss_test",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "expected_loss_control",
              "displayName": "expected_loss_control",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "sample_ratio_problem",
              "displayName": "sample_ratio_problem",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "sample_ratio_status",
              "displayName": "sample_ratio_status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2000,
        240
      ],
      "id": "9ca93ced-e9fc-43a4-8a03-af6153b61879",
      "name": "Append row in sheet1",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "dsUvNHMNlqw4axJe",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "content": "## JavaScript generator\nCreates function to fix data structure",
        "height": 304,
        "width": 288,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        704,
        128
      ],
      "typeVersion": 1,
      "id": "48492d38-b5d8-4ecc-aa08-447d490c5ff9",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=%INPUT%\n{{ JSON.stringify($input.all()) }}\n\nYou are a JavaScript expert. Write a function named `convertData` that transforms any input data into a fixed output structure.\n\n**Input:** The function receives a single argument `data`, which can be in any of the following formats:\n- An array of objects with a `json` key (e.g., `[{ json: { date, control_visitors, control_orders, variation_visitors, variation_orders } }]`).\n- An array of plain objects (e.g., `[{ date, control_visitors, ... }]`).\n- A single object with a `data` array (e.g., `{ data: [...] }`).\n- A single object with a `json` key containing a `data` array (e.g., `{ json: { data: [...] } }`).\n\n**Output:** Always return an array of objects, each having:\n- `json`: an object with the keys `date`, `control_visitors`, `control_orders`, `variation_visitors`, `variation_orders`.\n- `pairedItem`: an object with `item` (index) and `input` (always 0).\n\n%PREVIOUS_ERRORS%\nError: {{ $json.error }}\nDetails: {{ $json.details }}\nExpected: {{ $json.expected }}\n\n**Rules:**\n1. **NEVER embed concrete data values** (e.g., specific dates, numbers, or strings) in the code – only use the input data dynamically.\n2. Handle field name variations (e.g., `cntrol_visitors`, `cntl_visitors`) by remapping them to the required keys.\n3. Re-index `pairedItem.item` to match the output array position.\n4. If the input is already in the correct format, return it as-is (after re-indexing).\n5. If the input is empty or cannot be transformed, return an empty array.\n\nWrite only the function `convertData` – no additional code or comments.\n\n\n```javascript\n//Code\n```",
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        736,
        240
      ],
      "id": "9d580f62-6efd-4f32-9c57-3f3f0ecc2d3c",
      "name": "Structure fixer JS"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "b85f649b-5f28-45d5-a700-ca76f3bfd787",
              "name": "script",
              "value": "={{ $json.text.replace(/^```javascript\\s*/, '').replace(/\\s*```$/, '') }}",
              "type": "string"
            },
            {
              "id": "fd2670d0-6c9f-436f-9fa9-62910aaee1ea",
              "name": "data",
              "value": "={{ $('CSV to JSON').all().map(item => item.json) }}",
              "type": "array"
            },
            {
              "id": "6c07b541-4518-4701-b368-8167f127c574",
              "name": "errors",
              "value": "[]",
              "type": "array"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1040,
        240
      ],
      "id": "3f223f10-f99d-4754-ace8-9978d96f08e5",
      "name": "Variables set"
    },
    {
      "parameters": {
        "jsCode": "// The input is a single item with json.script and json.data\nconst item = $input.first().json;\n\n// Extract script and data\nconst scriptText = item.script;\nconst dataArray = item.data;\n\n// Remove any Markdown fences (if present)\nconst cleanCode = scriptText.replace(/^```javascript\\s*/, '').replace(/\\s*```$/, '');\n\n// Initialize errors array\nconst errors = [];\n\n// Evaluate the script – this defines the \"convertData\" function (exactly this name)\ntry {\n    eval(cleanCode);\n} catch (evalError) {\n    errors.push(`Script evaluation error: ${evalError.message}`);\n}\n\n// Check that convertData is defined\nif (typeof convertData !== 'function') {\n    errors.push(\"Function 'convertData' is not defined in the script.\");\n}\n\n// Only proceed if no errors so far\nlet resultItems = null;\nlet transformedData = null;\n\nif (errors.length === 0) {\n    // The function expects an array where the first element has json.data\n    const inputForFunction = [ { json: { data: dataArray } } ];\n\n    try {\n        // Call the function with the correct name\n        resultItems = convertData(inputForFunction);\n    } catch (execError) {\n        errors.push(`Execution error: ${execError.message}`);\n    }\n}\n\nif (errors.length === 0 && Array.isArray(resultItems)) {\n    // Extract only the inner json objects (flatten)\n    transformedData = resultItems.map(item => item.json);\n} else if (errors.length === 0 && !Array.isArray(resultItems)) {\n    errors.push(\"Function 'convertData' did not return an array.\");\n}\n\n// Validate length of converted vs original, errors: append to errors\nif( dataArray.length !== transformedData?.length ){\n  errors.push(\"Length mismatch, dataArray is\"+dataArray.length +\" and transformedData is\" + transformedData?.length+\". Possibly code truncating list?\")\n}\n\n\n// Validate no numbers in the actual function code\n// Check for numeric literals in the script (excluding those used as array indices or small numbers)\n// We'll use a regex that matches numbers with 2 or more digits, which are likely hardcoded data.\nconst hardcodedNumbers = cleanCode.match(/\\b\\d{2,}\\b/g);\nif (hardcodedNumbers && hardcodedNumbers.length > 0) {\n    errors.push(`Script contains hardcoded numbers: ${hardcodedNumbers.join(', ')}. The function must be dynamic.`);\n}\n// Return a single item (or multiple if you prefer)\nreturn { data: transformedData, errors: errors };"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1168,
        240
      ],
      "id": "e1404356-c8ff-4220-80e8-6541cdbff1ea",
      "name": "Validation and run JS structure fixes"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://abdecisions.com/api/processor",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"jsonData\": {{ $input.all().flatMap(i => i.json.data).toJsonString() }}\n}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1344,
        240
      ],
      "id": "5fd65c15-841a-4edd-aef6-8e751c17efa6",
      "name": "API Call"
    },
    {
      "parameters": {
        "jsCode": "// Grab the CSV text — adjust this path to match your input field\nconst csvText = $input.first().json.csv_data;\n\n// Split into \"rows\" on any whitespace (handles \\n, \\r\\n, or plain spaces)\nconst rows = csvText.trim().split(/\\s+/);\n\n// First row is the header\nconst headers = rows[0].split(',');\n\n// Build JSON objects for the rest\nconst results = [];\nfor (let i = 1; i < rows.length; i++) {\n  const values = rows[i].split(',');\n  const obj = {};\n  headers.forEach((header, idx) => {\n    const raw = values[idx];\n    // Convert numeric-looking values to numbers, keep everything else (e.g. dates) as strings\n    obj[header] = raw !== '' && !isNaN(raw) ? Number(raw) : raw;\n  });\n  results.push({ json: obj });\n}\n\nreturn results;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        64,
        240
      ],
      "id": "f060fca4-4218-497e-95e9-e6e6959f61d3",
      "name": "CSV to JSON"
    },
    {
      "parameters": {
        "content": "## Data ingest\nExample data, and Code node to convert CSV to JSON.",
        "height": 304,
        "width": 416
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -224,
        128
      ],
      "typeVersion": 1,
      "id": "fdd430d2-6ff3-4ff9-a81f-961bbeb9364d",
      "name": "Sticky Note4"
    },
    {
      "parameters": {
        "content": "## Limit data\nLM doesn't need all the data",
        "height": 304,
        "width": 166,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        208,
        128
      ],
      "typeVersion": 1,
      "id": "559a16a4-0fdd-4410-b7f6-aa0eb1674b14",
      "name": "Sticky Note5"
    },
    {
      "parameters": {
        "content": "## Flow Glue\nAggregate to single item and merge data from failures",
        "height": 304,
        "width": 278,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        128
      ],
      "typeVersion": 1,
      "id": "fa53bd14-a1aa-492e-95ca-c8d06a2669e6",
      "name": "Sticky Note6"
    },
    {
      "parameters": {
        "content": "## Convert data\nUses JS to fix data structure, and validates data",
        "height": 304,
        "width": 272,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1024,
        128
      ],
      "typeVersion": 1,
      "id": "9ba5eea7-ddb8-40d0-af03-ac22213054f9",
      "name": "Sticky Note7"
    },
    {
      "parameters": {
        "content": "## API Call and loop\nCall API, send any errors back to \"Merge\"",
        "height": 304,
        "width": 272,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1328,
        128
      ],
      "typeVersion": 1,
      "id": "8e2ec629-8d63-442a-9184-4d75216dd13b",
      "name": "Sticky Note8"
    },
    {
      "parameters": {
        "content": "## Update Google Sheet\nClears the current Google sheets, then prepares data for appending to Sheets",
        "height": 320,
        "width": 544
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1632,
        128
      ],
      "typeVersion": 1,
      "id": "702d08ef-1c22-4e43-bbde-0f2e6ce8ae92",
      "name": "Sticky Note9"
    }
  ],
  "pinData": {},
  "connections": {
    "Edit Fields3": {
      "main": [
        [
          {
            "node": "CSV to JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Limit1": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "Structure fixer JS",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Structure fixer JS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If": {
      "main": [
        [
          {
            "node": "Clear sheet1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "When clicking ‘Execute workflow’": {
      "main": [
        [
          {
            "node": "Edit Fields3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clear sheet1": {
      "main": [
        [
          {
            "node": "Edit Fields7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields7": {
      "main": [
        [
          {
            "node": "Code1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code1": {
      "main": [
        [
          {
            "node": "Append row in sheet1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Structure fixer JS": {
      "main": [
        [
          {
            "node": "Variables set",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Variables set": {
      "main": [
        [
          {
            "node": "Validation and run JS structure fixes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validation and run JS structure fixes": {
      "main": [
        [
          {
            "node": "API Call",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API Call": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CSV to JSON": {
      "main": [
        [
          {
            "node": "Limit1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "a7fc7f7e-9bd2-4bd2-b654-9d2324991ca7",
  "meta": {
    "instanceId": "a7d3c8fee44ed6dfa5d3bf2eca37e2240651cc218f5d2ad518615bd97f803884"
  },
  "id": "QoA7y73yxKZJT0lm",
  "tags": []
}