AutomationFlowsAI & RAG › Send Sales Forecast Charts and Answer Q&a on Whatsapp with Openai

Send Sales Forecast Charts and Answer Q&a on Whatsapp with Openai

ByLee Lin @l2l on n8n.io

Top Branch Workflow The Data Scientist: Ingest: Pulls historical sales data from Google Sheets. Math Engine: Runs 7 statistical algorithms (e.g., Seasonal Naive, Linear Trend, Regression). It backtests them against your history and scientifically selects the winner with the…

Cron / scheduled trigger★★★★★ complexityAI-powered31 nodesOutput Parser StructuredOpenAI ChatAgentWhatsAppGoogle SheetsHTTP RequestData TableWhatsApp Trigger
AI & RAG Trigger: Cron / scheduled Nodes: 31 Complexity: ★★★★★ AI nodes: yes Added:
Send Sales Forecast Charts and Answer Q&a on Whatsapp with Openai — n8n workflow card showing Output Parser Structured, OpenAI Chat, Agent integration

This workflow corresponds to n8n.io template #12289 — we link there as the canonical source.

This workflow follows the Agent → Datatable recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "dPckkM52faCVdhYi",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Automate sales forecasts visual report and Q&A on WhatsApp using OpenAI Agent",
  "tags": [],
  "nodes": [
    {
      "id": "6a05f550-9612-4ecc-9439-92194df42c22",
      "name": "Monthly Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -928,
        784
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "months",
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "1d400ae4-4b7f-4ec0-b8fb-51675e94521c",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        384,
        976
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"required\": [\n    \"forecastHorizon\",\n    \"forecastedSales\",\n    \"forecastingTechnique\",\n    \"confidenceLevel\",\n    \"rmse\",\n    \"mape\",\n    \"mapeFormatted\",\n    \"trend\",\n    \"seasonality\",\n    \"keyFactors\",\n    \"reasoning\"\n  ],\n  \"properties\": {\n    \"forecastHorizon\": {\n      \"type\": \"string\",\n      \"description\": \"Target period (e.g., '2020-04').\"\n    },\n    \"forecastedSales\": {\n      \"type\": \"number\",\n      \"description\": \"Projected sales figure.\"\n    },\n    \"forecastingTechnique\": {\n      \"type\": \"string\",\n      \"description\": \"Internal method name (keep technical for records).\"\n    },\n    \"confidenceLevel\": {\n      \"type\": \"string\",\n      \"enum\": [\"high\", \"medium\", \"low\"],\n      \"description\": \"Business confidence level.\"\n    },\n    \"rmse\": {\n      \"type\": \"number\",\n      \"description\": \"Technical error metric (kept for database, do not use in text).\"\n    },\n    \"mape\": {\n      \"type\": \"number\",\n      \"description\": \"Technical error % (kept for database, do not use in text).\"\n    },\n    \"mapeFormatted\": {\n      \"type\": \"string\",\n      \"description\": \"Formatted error %.\"\n    },\n    \"trend\": {\n      \"type\": \"string\",\n      \"enum\": [\"increasing\", \"decreasing\", \"stable\"],\n      \"description\": \"Direction of the business.\"\n    },\n    \"seasonality\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"detected\": { \"type\": \"boolean\" },\n        \"pattern\": { \"type\": \"string\" }\n      }\n    },\n    \"keyFactors\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\" },\n      \"description\": \"3-6 bullet points using business language (no jargon).\"\n    },\n    \"reasoning\": {\n      \"type\": \"string\",\n      \"description\": \"Executive Summary paragraph for a business director.\"\n    }\n  }\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "f884c524-4a97-45fa-ad2f-bf21592b63b6",
      "name": "Workflow Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        -752,
        784
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "spreadsheetId",
              "type": "string",
              "value": "SalesSheet"
            },
            {
              "id": "5532d7e6-3e56-42b3-8140-197eb4e71446",
              "name": "sheetName",
              "type": "string",
              "value": "Sales"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "63b081c2-8a77-48e7-988d-dea5183162cf",
      "name": "Forecast Engine",
      "type": "n8n-nodes-base.code",
      "position": [
        -128,
        784
      ],
      "parameters": {
        "jsCode": "/**\n * Forecast Engine (Deterministic) \u2014 Monthly Version\n *\n * INPUT (expects one item from Data Cleaning):\n * {\n * data: [\n * { Year: 2015, Month: 1, Sales: 138523, period: \"2015-01\" },\n * ...\n * ]\n * }\n *\n * OUTPUT (one item):\n * {\n * series: [{ period: \"2015-01\", Sales: ... }, ...],\n * candidates: [ ... ],\n * recommended: { ... forecastHorizon: \"2020-04\" ... }\n * }\n */\n\nfunction num(x) {\n  const n = typeof x === \"number\" ? x : parseFloat(String(x).replace(/,/g, \"\").trim());\n  return Number.isFinite(n) ? n : NaN;\n}\n\nfunction buildPeriod(y, m) {\n  return `${y}-${String(m).padStart(2, '0')}`;\n}\n\nfunction parsePeriod(p) {\n  // Regex for YYYY-MM\n  const m = /^(\\d{4})-(\\d{2})$/.exec(String(p).trim());\n  if (!m) return null;\n  return { year: parseInt(m[1], 10), month: parseInt(m[2], 10) };\n}\n\nfunction nextPeriodLabel(lastPeriod) {\n  const parsed = parsePeriod(lastPeriod);\n  if (!parsed) return \"next_period\";\n  let { year, month } = parsed;\n  month += 1;\n  if (month === 13) {\n    month = 1;\n    year += 1;\n  }\n  return buildPeriod(year, month);\n}\n\nfunction rmse(actual, pred) {\n  const errs = [];\n  for (let i = 0; i < actual.length; i++) {\n    const a = actual[i], p = pred[i];\n    if (!Number.isFinite(a) || !Number.isFinite(p)) continue;\n    errs.push((a - p) ** 2);\n  }\n  if (errs.length === 0) return null;\n  return Math.sqrt(errs.reduce((s, e) => s + e, 0) / errs.length);\n}\n\nfunction mapePct(actual, pred) {\n  const errs = [];\n  for (let i = 0; i < actual.length; i++) {\n    const a = actual[i], p = pred[i];\n    if (!Number.isFinite(a) || !Number.isFinite(p)) continue;\n    if (a === 0) continue; // skip zeros\n    errs.push(Math.abs((a - p) / a));\n  }\n  if (errs.length === 0) return null;\n  return (errs.reduce((s, e) => s + e, 0) / errs.length) * 100;\n}\n\nfunction formatPct(x, decimals = 1) {\n  if (x === null || x === undefined || !Number.isFinite(x)) return null;\n  return `${x.toFixed(decimals)}%`;\n}\n\nfunction dot(a, b) {\n  let s = 0;\n  for (let i = 0; i < a.length; i++) s += a[i] * b[i];\n  return s;\n}\n\n// Linear regression y = a + b*t\nfunction fitLinear(ts, ys) {\n  const n = ts.length;\n  if (n < 2) return null;\n\n  const tMean = ts.reduce((s, v) => s + v, 0) / n;\n  const yMean = ys.reduce((s, v) => s + v, 0) / n;\n\n  let nume = 0;\n  let deno = 0;\n  for (let i = 0; i < n; i++) {\n    const dt = ts[i] - tMean;\n    nume += dt * (ys[i] - yMean);\n    deno += dt * dt;\n  }\n  if (deno === 0) return null;\n\n  const b = nume / deno;\n  const a = yMean - b * tMean;\n  return { a, b };\n}\n\n// Simple exponential smoothing (level-only)\nfunction sesForecastOneStep(history, alpha) {\n  if (history.length === 0) return null;\n  let level = history[0];\n  for (let i = 1; i < history.length; i++) {\n    level = alpha * history[i] + (1 - alpha) * level;\n  }\n  return level;\n}\n\n// ----- OLS helpers (small-matrix Gauss-Jordan) -----\nfunction solveLinearSystem(A, b) {\n  const n = A.length;\n  const M = A.map((row, i) => row.slice().concat([b[i]]));\n\n  for (let col = 0; col < n; col++) {\n    let pivotRow = col;\n    for (let r = col + 1; r < n; r++) {\n      if (Math.abs(M[r][col]) > Math.abs(M[pivotRow][col])) pivotRow = r;\n    }\n    if (Math.abs(M[pivotRow][col]) < 1e-12) return null;\n\n    if (pivotRow !== col) {\n      const tmp = M[col];\n      M[col] = M[pivotRow];\n      M[pivotRow] = tmp;\n    }\n\n    const pivot = M[col][col];\n    for (let c = col; c <= n; c++) M[col][c] /= pivot;\n\n    for (let r = 0; r < n; r++) {\n      if (r === col) continue;\n      const factor = M[r][col];\n      for (let c = col; c <= n; c++) M[r][c] -= factor * M[col][c];\n    }\n  }\n\n  return M.map((row) => row[n]);\n}\n\nfunction fitOLS(X, y, ridge = 1e-8) {\n  const n = X.length;\n  const k = X[0].length;\n\n  const XtX = Array.from({ length: k }, () => Array(k).fill(0));\n  const Xty = Array(k).fill(0);\n\n  for (let i = 0; i < n; i++) {\n    const xi = X[i];\n    const yi = y[i];\n    for (let a = 0; a < k; a++) {\n      Xty[a] += xi[a] * yi;\n      for (let b = 0; b < k; b++) {\n        XtX[a][b] += xi[a] * xi[b];\n      }\n    }\n  }\n\n  for (let d = 0; d < k; d++) XtX[d][d] += ridge;\n\n  return solveLinearSystem(XtX, Xty);\n}\n\n// Monthly Dummies: 11 dummies relative to January base\nfunction monthDummies(m) {\n  // m is 1..12\n  // Jan (1) -> all zeros\n  // Feb (2) -> index 0 is 1\n  // ...\n  // Dec (12) -> index 10 is 1\n  const dummies = Array(11).fill(0);\n  if (m > 1 && m <= 12) {\n    dummies[m - 2] = 1;\n  }\n  return dummies;\n}\n\n// -------------------- MAIN --------------------\nconst input = items[0]?.json ?? {};\nconst raw = Array.isArray(input.data) ? input.data : [];\n\n// Require at least 2 full years (24 months) for reliable seasonality\nif (raw.length < 24) {\n  // Warn but proceed if > 13, else throw\n  if (raw.length < 13) {\n    throw new Error(\"Not enough data. Provide at least 13 months for monthly seasonality (ideally 24+).\");\n  }\n}\n\n// Normalize + sort\nconst series = raw\n  .map((r) => {\n    const y = r.Year ?? r.year;\n    const m = r.Month ?? r.month; // Read Month\n    const s = num(r.Sales ?? r.sales);\n    const period = r.period ? String(r.period) : buildPeriod(y, m);\n    const parsed = parsePeriod(period);\n    return {\n      period,\n      year: parsed?.year ?? y,\n      month: parsed?.month ?? m,\n      Sales: s,\n    };\n  })\n  .filter((r) => Number.isFinite(r.year) && Number.isFinite(r.month) && Number.isFinite(r.Sales))\n  .sort((a, b) => (a.year - b.year) || (a.month - b.month));\n\nconst y = series.map((r) => r.Sales);\nconst periods = series.map((r) => r.period);\n\nconst seasonLag = 12;             // Monthly seasonality\nconst maWindow = 3;               // Short-term moving average (3 months)\nconst alphas = [0.2, 0.4, 0.6, 0.8];\nconst minBacktestPoints = 6;\n\n// Rolling one-step backtest: train on [0..t-1], predict y[t]\nfunction backtest(methodName) {\n  const actual = [];\n  const pred = [];\n\n  for (let t = 0; t < y.length; t++) {\n    const hist = y.slice(0, t);\n    let yhat = null;\n\n    if (methodName === \"seasonal_naive\") {\n      if (t - seasonLag >= 0) yhat = y[t - seasonLag];\n    }\n\n    if (methodName === \"seasonal_average\") {\n      const m = series[t].month;\n      const vals = series\n        .slice(0, t)\n        .filter((r) => r.month === m) // Filter by same month\n        .map((r) => r.Sales);\n      if (vals.length >= 1) yhat = vals.reduce((s, v) => s + v, 0) / vals.length;\n    }\n\n    if (methodName === \"moving_average\") {\n      if (hist.length >= 1) {\n        const window = hist.slice(-Math.min(maWindow, hist.length));\n        yhat = window.reduce((s, v) => s + v, 0) / window.length;\n      }\n    }\n\n    if (methodName === \"trend_linear\") {\n      if (hist.length >= 3) {\n        const ts = hist.map((_, i) => i);\n        const fit = fitLinear(ts, hist);\n        if (fit) {\n          const nextT = hist.length;\n          yhat = fit.a + fit.b * nextT;\n        }\n      }\n    }\n\n    if (methodName === \"exp_smoothing\") {\n      if (hist.length >= 2) {\n        let bestAlpha = null;\n        let bestErr = Infinity;\n\n        for (const a of alphas) {\n          const aAct = [];\n          const aPred = [];\n          for (let i = 1; i < hist.length; i++) {\n            const h = hist.slice(0, i);\n            const f = sesForecastOneStep(h, a);\n            if (Number.isFinite(f)) {\n              aAct.push(hist[i]);\n              aPred.push(f);\n            }\n          }\n          const e = rmse(aAct, aPred);\n          if (e !== null && e < bestErr) {\n            bestErr = e;\n            bestAlpha = a;\n          }\n        }\n\n        if (bestAlpha !== null) {\n          yhat = sesForecastOneStep(hist, bestAlpha);\n        }\n      }\n    }\n\n    if (methodName === \"seasonal_regression\") {\n      // intercept + 11 month dummies = 12 params. Need > 13 points.\n      if (hist.length >= 15) {\n        const X = [];\n        const Y = [];\n        for (let i = 0; i < hist.length; i++) {\n          const d = monthDummies(series[i].month);\n          X.push([1, ...d]);\n          Y.push(hist[i]);\n        }\n        const beta = fitOLS(X, Y);\n        if (beta) {\n          const dT = monthDummies(series[t].month);\n          yhat = dot(beta, [1, ...dT]);\n        }\n      }\n    }\n\n    if (methodName === \"seasonal_regression_trend\") {\n      // intercept + time + 11 dummies = 13 params. Need > 14 points.\n      if (hist.length >= 16) {\n        const X = [];\n        const Y = [];\n        for (let i = 0; i < hist.length; i++) {\n          const d = monthDummies(series[i].month);\n          X.push([1, i, ...d]);\n          Y.push(hist[i]);\n        }\n        const beta = fitOLS(X, Y);\n        if (beta) {\n          const dT = monthDummies(series[t].month);\n          yhat = dot(beta, [1, hist.length, ...dT]);\n        }\n      }\n    }\n\n    if (Number.isFinite(yhat)) {\n      actual.push(y[t]);\n      pred.push(yhat);\n    }\n  }\n\n  if (actual.length < minBacktestPoints) return null;\n\n  const r = rmse(actual, pred);\n  const m = mapePct(actual, pred);\n\n  return {\n    method: methodName,\n    backtest_points: actual.length,\n    rmse: r,\n    mape: m,\n    mapeFormatted: formatPct(m, 1),\n  };\n}\n\nconst methods = [\n  \"seasonal_naive\",\n  \"seasonal_average\",\n  \"moving_average\",\n  \"trend_linear\",\n  \"exp_smoothing\",\n  \"seasonal_regression\",\n  \"seasonal_regression_trend\",\n];\n\nconst candidates = methods.map((m) => backtest(m)).filter(Boolean);\n\nif (candidates.length === 0) {\n  throw new Error(\"No method had enough backtest points. Need more historical monthly data.\");\n}\n\n// Choose best: lowest MAPE, tie-breaker lowest RMSE\ncandidates.sort((a, b) => {\n  const aM = a.mape, bM = b.mape;\n  const aR = a.rmse, bR = b.rmse;\n\n  if (Number.isFinite(aM) && Number.isFinite(bM)) {\n    if (aM !== bM) return aM - bM;\n    return (aR ?? Infinity) - (bR ?? Infinity);\n  }\n  if (Number.isFinite(aM) && !Number.isFinite(bM)) return -1;\n  if (!Number.isFinite(aM) && Number.isFinite(bM)) return 1;\n  return (aR ?? Infinity) - (bR ?? Infinity);\n});\n\nconst best = candidates[0];\n\n// Forecast next month using best method on full history\nfunction forecastNext(methodName) {\n  const n = y.length;\n\n  if (methodName === \"seasonal_naive\") {\n    return n - seasonLag >= 0 ? y[n - seasonLag] : null;\n  }\n\n  if (methodName === \"seasonal_average\") {\n    const lastM = series[n - 1].month;\n    let nextM = lastM + 1;\n    if (nextM === 13) nextM = 1;\n\n    const vals = series.filter((r) => r.month === nextM).map((r) => r.Sales);\n    return vals.length ? vals.reduce((s, v) => s + v, 0) / vals.length : null;\n  }\n\n  if (methodName === \"moving_average\") {\n    const window = y.slice(-Math.min(maWindow, n));\n    return window.reduce((s, v) => s + v, 0) / window.length;\n  }\n\n  if (methodName === \"trend_linear\") {\n    const ts = y.map((_, i) => i);\n    const fit = fitLinear(ts, y);\n    if (!fit) return null;\n    return fit.a + fit.b * n;\n  }\n\n  if (methodName === \"exp_smoothing\") {\n    let bestAlpha = null;\n    let bestErr = Infinity;\n\n    for (const a of alphas) {\n      const aAct = [];\n      const aPred = [];\n      for (let i = 1; i < y.length; i++) {\n        const h = y.slice(0, i);\n        const f = sesForecastOneStep(h, a);\n        if (Number.isFinite(f)) {\n          aAct.push(y[i]);\n          aPred.push(f);\n        }\n      }\n      const e = rmse(aAct, aPred);\n      if (e !== null && e < bestErr) {\n        bestErr = e;\n        bestAlpha = a;\n      }\n    }\n\n    return bestAlpha === null ? null : sesForecastOneStep(y, bestAlpha);\n  }\n\n  if (methodName === \"seasonal_regression\") {\n    if (y.length < 15) return null;\n\n    const X = [];\n    const Y = [];\n    for (let i = 0; i < y.length; i++) {\n      const d = monthDummies(series[i].month);\n      X.push([1, ...d]);\n      Y.push(y[i]);\n    }\n    const beta = fitOLS(X, Y);\n    if (!beta) return null;\n\n    let nextM = series[series.length - 1].month + 1;\n    if (nextM === 13) nextM = 1;\n    const dN = monthDummies(nextM);\n    return dot(beta, [1, ...dN]);\n  }\n\n  if (methodName === \"seasonal_regression_trend\") {\n    if (y.length < 16) return null;\n\n    const X = [];\n    const Y = [];\n    for (let i = 0; i < y.length; i++) {\n      const d = monthDummies(series[i].month);\n      X.push([1, i, ...d]);\n      Y.push(y[i]);\n    }\n    const beta = fitOLS(X, Y);\n    if (!beta) return null;\n\n    let nextM = series[series.length - 1].month + 1;\n    if (nextM === 13) nextM = 1;\n    const dN = monthDummies(nextM);\n\n    const nextIndex = y.length;\n    return dot(beta, [1, nextIndex, ...dN]);\n  }\n\n  return null;\n}\n\nconst forecastedSales = forecastNext(best.method);\nif (!Number.isFinite(forecastedSales)) {\n  throw new Error(`Best method (${best.method}) could not produce a next-month forecast.`);\n}\n\nconst forecastHorizon = nextPeriodLabel(periods[periods.length - 1]);\n\n// ---- Presentation rounding ----\nconst forecastedSalesRounded = Math.round(forecastedSales);\nconst rmseRounded = Number.isFinite(best.rmse) ? Math.round(best.rmse) : best.rmse;\nconst mapeRounded = Number.isFinite(best.mape) ? Number(best.mape.toFixed(1)) : best.mape;\nconst mapeFormattedRounded = Number.isFinite(mapeRounded) ? `${mapeRounded}%` : best.mapeFormatted;\n\nreturn [\n  {\n    json: {\n      series: series.map((r) => ({ period: r.period, Sales: r.Sales })),\n      candidates,\n      recommended: {\n        forecastHorizon,\n        forecastedSales: forecastedSalesRounded,\n        forecastingTechnique: best.method,\n        rmse: rmseRounded,\n        mape: mapeRounded,\n        mapeFormatted: mapeFormattedRounded,\n        backtestPoints: best.backtest_points,\n      },\n      selectionRule: \"lowest MAPE; tie-breaker RMSE\",\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "492ffd7e-204b-4efc-9504-8c881b15e808",
      "name": "Data Cleaning",
      "type": "n8n-nodes-base.code",
      "position": [
        -400,
        784
      ],
      "parameters": {
        "jsCode": "// n8n Code node: Run Once for All Items\n\nfunction toNumber(v) {\n  const n = typeof v === 'number' ? v : parseFloat(String(v).trim());\n  return Number.isFinite(n) ? n : NaN;\n}\n\nconst rows = items\n  .map(i => i.json)\n  .filter(r =>\n    Number.isFinite(toNumber(r.Year)) &&\n    Number.isFinite(toNumber(r.Month)) &&         // Changed: Check for Month\n    Number.isFinite(toNumber(r.MonthlySales))     // Changed: Check for MonthlySales\n  )\n  .map(r => ({\n    ...r,\n    Year: toNumber(r.Year),\n    Month: toNumber(r.Month),                     // Changed: Store Month\n    Sales: toNumber(r.MonthlySales),              // Changed: Map input 'MonthlySales' to generic 'Sales' for downstream compatibility\n    period: `${toNumber(r.Year)}-${String(toNumber(r.Month)).padStart(2, '0')}` // Changed: Format period as YYYY-MM (e.g., 2015-01)\n  }))\n  .sort((a, b) => (a.Year - b.Year) || (a.Month - b.Month)); // Changed: Sort by Month\n\n// Output ONE item that contains the FULL dataset as an array:\nreturn [\n  {\n    json: {\n      data: rows,\n      rowCount: rows.length\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "91e7c95e-b0c3-432b-9a1e-c1ce95ab28ef",
      "name": "OpenAI Chat Model1",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        160,
        976
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5",
          "cachedResultName": "gpt-5"
        },
        "options": {
          "timeout": 180000
        },
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "f513b665-26c8-4297-988e-368a8883a1e2",
      "name": "OpenAI Chat Model2",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        464,
        1632
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-nano",
          "cachedResultName": "gpt-4.1-nano"
        },
        "options": {},
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "c484ca60-b162-4bc1-bba9-e65fa0299236",
      "name": "Q&A AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        496,
        1376
      ],
      "parameters": {
        "text": "={{ $('Normalize Whatsapp Input').item.json.chatInput }}",
        "options": {
          "systemMessage": "=You are a Strategic Business Advisor that answers questions about the latest monthly sales forecast. You speak in a clear, confident, executive tone.\n\nContext:\n- Latest forecast target: {{$json.forecastPeriod}}\n- Forecasted sales: {{$json.forecastSales}}\n- Executive Summary: {{$json.summaryReport}}\n\nHistorical sales data (Year-Month: Sales):\n{{ $json.historySalesParsed.map(i => `${i.year}-${String(i.month).padStart(2, '0')}: ${i.sales}`).join(', ') }}"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "d920386c-40b1-4f80-9ee4-ffb7a0aba30c",
      "name": "Normalize Whatsapp Input",
      "type": "n8n-nodes-base.set",
      "position": [
        -144,
        1376
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "6814165f-1ff0-4654-8ec5-0fad100c3c95",
              "name": "chatInput",
              "type": "string",
              "value": "={{\n  (\n    ($json.messages && $json.messages[0] && $json.messages[0].text && $json.messages[0].text.body) ||\n    ($json.value && $json.value.messages && $json.value.messages[0] && $json.value.messages[0].text && $json.value.messages[0].text.body) ||\n    ($json.entry && $json.entry[0] && $json.entry[0].changes && $json.entry[0].changes[0] && $json.entry[0].changes[0].value &&\n      $json.entry[0].changes[0].value.messages && $json.entry[0].changes[0].value.messages[0] &&\n      $json.entry[0].changes[0].value.messages[0].text && $json.entry[0].changes[0].value.messages[0].text.body) ||\n    \"\"\n  )\n}}\n"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "8878ba94-af04-4331-80a1-e86da10aa423",
      "name": "Q&A Message",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        800,
        1376
      ],
      "parameters": {
        "textBody": "={{ $json.output }}",
        "operation": "send",
        "phoneNumberId": "978279622026190",
        "additionalFields": {},
        "recipientPhoneNumber": "1234"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "ad26c654-567d-4f12-b2ac-ee08dfaef020",
      "name": "Get Sales Data",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -560,
        784
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 402921456,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1GqPjqmHcl-WZcadEoFy8idUnOsRiYTQce0Y0fC6uyYk/edit#gid=402921456",
          "cachedResultName": "Sample_Monthly_Sales.csv"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1GqPjqmHcl-WZcadEoFy8idUnOsRiYTQce0Y0fC6uyYk",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1GqPjqmHcl-WZcadEoFy8idUnOsRiYTQce0Y0fC6uyYk/edit?usp=drivesdk",
          "cachedResultName": "Sample_Monthly_Sales"
        },
        "authentication": "serviceAccount"
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "15859013-73a8-40ce-9a19-23b2ad67bdc0",
      "name": "QuickChart",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        960,
        784
      ],
      "parameters": {
        "url": "https://quickchart.io/chart",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "chart"
            }
          }
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "chart",
              "value": "={{ JSON.stringify($json.chartConfig) }}"
            },
            {
              "name": "format",
              "value": "png"
            },
            {
              "name": "width",
              "value": "1200"
            },
            {
              "name": "height",
              "value": "600"
            },
            {
              "name": "backgroundColor",
              "value": "white"
            }
          ]
        }
      },
      "typeVersion": 4.3,
      "alwaysOutputData": false
    },
    {
      "id": "2d969ee0-70f1-467d-bb25-55fb58ea6f76",
      "name": "Upsert Latest Forecast",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        1248,
        672
      ],
      "parameters": {
        "columns": {
          "value": {
            "key": "latest",
            "historySales": "={{ JSON.stringify($items(\"Get Sales Data\").map(i => ({\n  year: i.json.Year,\n  month: i.json.Month,\n  sales: i.json.MonthlySales\n}))) }}",
            "forecastSales": "={{ $json.output.forecastedSales }}",
            "summaryReport": "={{ $json.summaryReport }}",
            "forecastPeriod": "={{ $json.output.forecastHorizon }}"
          },
          "schema": [
            {
              "id": "forecastPeriod",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "forecastPeriod",
              "defaultMatch": false
            },
            {
              "id": "forecastSales",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "forecastSales",
              "defaultMatch": false
            },
            {
              "id": "summaryReport",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "summaryReport",
              "defaultMatch": false
            },
            {
              "id": "key",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "key",
              "defaultMatch": false
            },
            {
              "id": "historySales",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "historySales",
              "defaultMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "filters": {
          "conditions": [
            {
              "keyName": "key",
              "keyValue": "latest"
            }
          ]
        },
        "options": {},
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "mTbTawxCGmZ8kp0A",
          "cachedResultUrl": "/projects/HgU76KeJzQXHrABf/datatables/mTbTawxCGmZ8kp0A",
          "cachedResultName": "Latest_Forecast"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ccfe9476-d0d9-44b7-bea9-fd1912ae9ea7",
      "name": "Get Latest Forecast",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        80,
        1376
      ],
      "parameters": {
        "filters": {
          "conditions": [
            {
              "keyName": "key",
              "keyValue": "latest"
            }
          ]
        },
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "mTbTawxCGmZ8kp0A",
          "cachedResultUrl": "/projects/HgU76KeJzQXHrABf/datatables/mTbTawxCGmZ8kp0A",
          "cachedResultName": "Latest_Forecast"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "a4ef98c6-2055-49a4-ae4e-0d99fbe5040b",
      "name": "Parsing Input",
      "type": "n8n-nodes-base.code",
      "position": [
        304,
        1376
      ],
      "parameters": {
        "jsCode": "const row = $json;\n\nlet history = [];\ntry {\n  history = JSON.parse(row.historySales || \"[]\");\n} catch (e) {\n  history = [];\n}\n\n// SAFETY STEP: Normalize keys to ensure lowercase 'year', 'month', 'sales'\n// This prevents errors if the database accidentally has \"Year\" or \"MonthlySales\"\nconst historyNormalized = history.map(item => ({\n  year: Number(item.year || item.Year),\n  month: Number(item.month || item.Month), \n  sales: Number(item.sales || item.Sales || item.MonthlySales)\n}));\n\nreturn [\n  {\n    json: {\n      ...row,\n      historySalesParsed: historyNormalized,\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "96ad6de4-4551-4f14-8f00-cd88e1033bb4",
      "name": "WhatsApp Trigger",
      "type": "n8n-nodes-base.whatsAppTrigger",
      "position": [
        -528,
        1392
      ],
      "parameters": {
        "options": {},
        "updates": [
          "messages"
        ]
      },
      "credentials": {
        "whatsAppTriggerApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "86c29b81-b78d-4fb8-b2a7-ec5905dff0bb",
      "name": "Sticky Note15",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -992,
        -112
      ],
      "parameters": {
        "width": 1376,
        "height": 608,
        "content": "## How It Works \n***Top Branch Workflow***\n\n**1. The Data Scientist:** \n   - **Ingest:** Pulls historical sales data from Google Sheets.\n   - **Math Engine:** Runs 7 statistical algorithms (e.g., Seasonal Naive, Linear Trend, Regression). It backtests them against your history and scientifically selects the winner with the lowest error rate.\n\n\n**2. The Data Analyst:** \n   - **Interpret:** The AI Agent takes the mathematical output and translates it into business insights, assigning confidence scores based on error margins.\n   - **Report:** Generates a visual trend chart (PNG) and sends a complete briefing to your phone.\n\n\n***Bottom Branch Workflow***\n\n**3. The Consultant:** AI Agent 2 handles the follow-up questions. It pulls the latest analysis context and checks historical rate data to give an informed answer.\n   - **Recall:** When you ask a question via WhatsApp, the bot retrieves the saved forecast state.\n   - **Answer:** It acts as an on-demand analyst, comparing current forecasts against historical actuals to give you instant answers.\n\n\n\n## Setup Steps\n1) Google Sheet: Prepare columns: Year, Month, Sales. Map the Sheet ID in the \"Workflow Configuration\" node.\n2) Forecast Engine: No config needed. It automatically detects seasonality vs. linear trends.\n3) Database: Create a table latest_forecast to store the JSON output.\n4) Credentials: Connect Google Sheets, OpenAI, and WhatsApp"
      },
      "typeVersion": 1
    },
    {
      "id": "6d5564ae-4e40-4a02-8b06-278cb7ddea86",
      "name": "Sticky Note17",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        336
      ],
      "parameters": {
        "color": 3,
        "width": 368,
        "height": 96,
        "content": "## Prerequisites\nData: At least 12 periods of historical sales data."
      },
      "typeVersion": 1
    },
    {
      "id": "81dd5053-bbca-4b21-8757-844d770d0af6",
      "name": "Sticky Note18",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        -112
      ],
      "parameters": {
        "color": 5,
        "width": 992,
        "height": 416,
        "content": "## Use Cases & Benefits\n**Business Owners:** \nGain enterprise-grade forecasting on autopilot. Always have a sophisticated financial outlook running in the background 24/7.\n\n**Sales Leaders:** \nGet immediate visibility into future revenue trends. Bypass the wait for end-of-month manual reports and get a strategic \"pulse check\" delivered instantly to your phone.\n\n\n\ud83e\udd16***Virtual Data Team:*** Instantly add the capabilities of a Data Scientist and Data Analyst to your business or division. It works alongside your existing team to handle the heavy lifting, or stands in as your dedicated automated department.\n\n\ud83e\udde0***Precision & Trust:*** Combines the best of both worlds: rigorous, deterministic code for the math (no hallucinations) and advanced AI for the strategic explanation. You get numbers you can trust with context you can use.\n\n\u26a1***Decision-Ready Insights:*** Stop digging through dashboards. High-level intelligence is pushed directly to you on WhatsApp, allowing you to make faster, data-driven decisions from anywhere.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "594c9672-202b-4b4b-921e-20251df60552",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -992,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 736,
        "height": 656,
        "content": "## 1. Data Pipeline\n**ETL & Normalization** Fetches raw Google Sheets data and normalizes formats to ensure the statistical engine receives clean inputs."
      },
      "typeVersion": 1
    },
    {
      "id": "6e719e61-b625-477f-ac99-464bf192d377",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -224,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 288,
        "height": 656,
        "content": "## 2. Forecasting\n**Statistical Tournament** Runs 7 distinct models (Seasonal Regression, Exp Smoothing, Moving Avg, etc.). It performs a rolling backtest to calculate RMSE & MAPE, strictly selecting the mathematically superior model."
      },
      "typeVersion": 1
    },
    {
      "id": "eda9e598-4b70-4793-a08b-092a933bb820",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        112,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 1008,
        "height": 656,
        "content": "## 3. AI Intelligence & Visualization\n**Synthesis & Rendering** The AI interprets the math into a business narrative, while QuickChart renders the data points into a visual trend graph PNG."
      },
      "typeVersion": 1
    },
    {
      "id": "84485933-8556-442e-ad7b-209bf8350ed0",
      "name": "Analyst AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        208,
        784
      ],
      "parameters": {
        "text": "={{ $json }}",
        "options": {
          "systemMessage": "You are a Strategic Business Advisor presenting a sales forecast to a Sales Director. You will receive technical forecasting data, and your job is to translate it into a clear, actionable Executive Summary.\n\nInput fields:\n- series: historical data [{ period: \"YYYY-MM\", Sales: number }, ...]\n- candidates: technical model evaluation metrics\n- recommended: the best model's output { forecastHorizon, forecastedSales, rmse, mape, mapeFormatted, ... }\n\nCritical rules:\n1) DO NOT change the forecastedSales number. Use it exactly as provided.\n2) STRICTLY AVOID technical jargon in the text fields (\"keyFactors\" and \"reasoning\").\n   - \ud83d\udeab Do not use: \"RMSE\", \"MAPE\", \"OLS\", \"Regression\", \"Backtesting\", \"Dummy variables\".\n   - \u2705 Do use: \"Historical accuracy\", \"Margin of error\", \"Recurring seasonal patterns\", \"Growth trend\".\n3) Translate metrics into business terms:\n   - Instead of \"MAPE of 4.8%\", say \"expected accuracy of ~95%\" or \"margin of error of \u00b15%\".\n4) Output ONLY a single JSON object matching the schema.\n\nYour tasks:\nA) Populate technical fields (forecastHorizon, forecastedSales, etc.) directly from 'recommended'.\nB) Determine 'confidenceLevel' (High/Medium/Low) based on MAPE (High if <5%, Med if <10%, Low if >10%).\nC) Determine 'trend' (Increasing/Decreasing/Stable) by comparing the average of the last 12 months vs the previous 12 months.\nD) 'seasonality': detected = true if method contains \"seasonal\"; pattern = \"monthly\".\n\nE) 'keyFactors': Provide 3\u20136 business-oriented bullet points explaining the drivers.\n   - Focus on visible behaviors in the data (e.g., \"Sales consistently peak in May,\" \"We are seeing steady Year-Over-Year growth\").\n   - Explain *why* the forecast is reliable (e.g., \"This projection accounts for the strong seasonal cycles observed in historical performance\").\n\nF) 'reasoning': Write this as a cohesive executive summary.\n   - Start with the headline numbers (Forecast Target and Date).\n   - Briefly explain the primary driver (e.g., \"Driven by strong seasonal momentum...\").\n   - State the confidence level in plain English (e.g., \"We have high confidence in this number due to a historical accuracy rate of X%\").\n\nReturn the final JSON object only."
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3
    },
    {
      "id": "dec6aeb6-ae13-4abf-a4b1-4884ebf755a6",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1152,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 288,
        "height": 656,
        "content": "## 4. Delivery & Storage\n**Notification System** Saves the forecast context to the database for future reference and pushes the final Report + Chart to WhatsApp."
      },
      "typeVersion": 1
    },
    {
      "id": "9425a977-d1cd-42a1-bd46-aa2d06a0d262",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -592,
        1248
      ],
      "parameters": {
        "color": 7,
        "width": 1664,
        "height": 560,
        "content": "## 5. Follow-Up Q&A with Sales Forecast AI Agent \n**Data Consultant** Retrieves the stored forecast data to answer user questions, acting as an on-demand analyst with full knowledge of the report."
      },
      "typeVersion": 1
    },
    {
      "id": "6d207b4b-4960-4a4e-ac45-4c05b75f37bf",
      "name": "Format AI Output",
      "type": "n8n-nodes-base.set",
      "position": [
        528,
        784
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "e0d9a155-f6b2-44cf-bc61-e754ba3144bf",
              "name": "summaryReport",
              "type": "string",
              "value": "={{ $json.output.reasoning }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "9497eaf3-d9ef-42ab-873b-4305daadf8ee",
      "name": "Filter Text Messages",
      "type": "n8n-nodes-base.if",
      "position": [
        -352,
        1392
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "be96ecef-2db9-48a8-b178-3d48112eae7b",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.messages[0].from}}",
              "rightValue": "16727551224"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "824dbfe8-bfb1-4083-9268-9817dcc73397",
      "name": "Combine Data for Chart",
      "type": "n8n-nodes-base.merge",
      "position": [
        640,
        976
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "b3597f68-b5d2-406b-b87b-bc6b2e3f3f7a",
      "name": "Send WhatsApp Report",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        1248,
        928
      ],
      "parameters": {
        "mediaPath": "useMedian8n",
        "operation": "send",
        "messageType": "image",
        "phoneNumberId": "978279622026190",
        "additionalFields": {
          "mediaCaption": "={{ $json.summaryReport }}"
        },
        "mediaPropertyName": "chart",
        "recipientPhoneNumber": "1234"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "5fafef97-73b1-400c-a397-fef285ef9046",
      "name": "Prepare Chart Config",
      "type": "n8n-nodes-base.code",
      "position": [
        784,
        784
      ],
      "parameters": {
        "jsCode": "const data = items[0].json.data;\nconst forecast = items[0].json.output;\n\nif (!Array.isArray(data) || data.length === 0) {\n  throw new Error(\"json.data is missing or empty.\");\n}\n\nconst labels = data.map(d => d.period);\nconst actual = data.map(d => Number(d.Sales));\n\n// --- CHANGE START ---\n// Get the very last historical sales value\nconst lastActualValue = actual[actual.length - 1];\nconst forecastValue = Number(forecast.forecastedSales);\n\n// Create forecast series:\n// 1. Fill with 'null' for all periods except the last two.\n// 2. Place the last historical value at the second-to-last position.\n// 3. Place the forecast value at the very last position.\nconst forecastSeries = Array(actual.length - 1).fill(null);\nforecastSeries.push(lastActualValue);\nforecastSeries.push(forecastValue);\n// --- CHANGE END ---\n\nconst nextPeriod = forecast?.forecastHorizon || \"Next\";\nconst labelsWithForecast = labels.concat([nextPeriod]);\n\n// The actual series still needs to end with 'null' so the blue line stops.\nconst actualWithNull = actual.concat([null]);\n\nconst chartConfig = {\n  type: \"line\",\n  data: {\n    labels: labelsWithForecast,\n    datasets: [\n      {\n        label: \"Historical Sales\",\n        data: actualWithNull,\n        fill: false,\n        tension: 0.2,\n        borderColor: \"rgb(54, 162, 235)\",\n        backgroundColor: \"rgba(54, 162, 235, 0.5)\",\n        pointRadius: 3 // Standard size for historical points\n      },\n      {\n        label: \"Forecast (Next Month)\",\n        data: forecastSeries,\n        fill: false,\n        tension: 0.2,\n        borderDash: [6, 6], // The connecting line will be dashed\n        borderColor: \"rgb(255, 99, 132)\", // Pink line color\n        backgroundColor: \"rgba(255, 99, 132, 0.5)\",\n        pointRadius: 5, // Make the forecast dot larger\n        pointHoverRadius: 7\n      }\n    ]\n  },\n  options: {\n    responsive: true,\n    plugins: {\n      title: { display: true, text: \"Monthly Sales (Historical + Forecast)\" },\n      legend: { display: true },\n      // Optional: Add a tooltip callback to label the forecast clearly\n      tooltip: {\n        callbacks: {\n          label: function(context) {\n            let label = context.dataset.label || '';\n            if (label) {\n                label += ': ';\n            }\n            if (context.parsed.y !== null) {\n                label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(context.parsed.y);\n            }\n            return label;\n          }\n        }\n      }\n    },\n    scales: {\n      y: {\n        title: { display: true, text: \"Sales\" },\n        beginAtZero: false\n      },\n      x: {\n        title: { display: true, text: \"Month\" },\n        ticks: {\n          autoSkip: true,\n          maxTicksLimit: 20\n        }\n      }\n    }\n  }\n};\n\nreturn [{ json: { ...items[0].json, chartConfig } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "29b1ae06-e5f4-464f-9ff7-f08c52acfae2",
      "name": "Simple Memory",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        624,
        1632
      ],
      "parameters": {
        "sessionKey": "={{ $('WhatsApp Trigger').item.json.messages[0].from }}",
        "sessionIdType": "customKey"
      },
      "typeVersion": 1.3
    }
  ],
  "active": false,
  "settings": {
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "c282cf4e-c15f-4056-be0f-a3e56b97be7c",
  "connections": {
    "QuickChart": {
      "main": [
        [
          {
            "node": "Upsert Latest Forecast",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send WhatsApp Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Q&A AI Agent": {
      "main": [
        [
          {
            "node": "Q&A Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Data Cleaning": {
      "main": [
        [
          {
            "node": "Combine Data for Chart",
            "type": "main",
            "index": 1
          },
          {
            "node": "Forecast Engine",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parsing Input": {
      "main": [
        [
          {
            "node": "Q&A AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory": {
      "ai_memory": [
        [
          {
            "node": "Q&A AI Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Get Sales Data": {
      "main": [
        [
          {
            "node": "Data Cleaning",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Forecast Engine": {
      "main": [
        [
          {
            "node": "Analyst AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyst AI Agent": {
      "main": [
        [
          {
            "node": "Format AI Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format AI Output": {
      "main": [
        [
          {
            "node": "Combine Data for Chart",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Monthly Schedule": {
      "main": [
        [
          {
            "node": "Workflow Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "WhatsApp Trigger": {
      "main": [
        [
          {
            "node": "Filter Text Messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "Analyst AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model2": {
      "ai_languageModel": [
        [
          {
            "node": "Q&A AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Get Latest Forecast": {
      "main": [
        [
          {
            "node": "Parsing Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Text Messages": {
      "main": [
        [
          {
            "node": "Normalize Whatsapp Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Chart Config": {
      "main": [
        [
          {
            "node": "QuickChart",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Data for Chart": {
      "main": [
        [
          {
            "node": "Prepare Chart Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Latest Forecast": {
      "main": [
        []
      ]
    },
    "Workflow Configuration": {
      "main": [
        [
          {
            "node": "Get Sales Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Whatsapp Input": {
      "main": [
        [
          {
            "node": "Get Latest Forecast",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Analyst AI Agent",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Top Branch Workflow The Data Scientist: Ingest: Pulls historical sales data from Google Sheets. Math Engine: Runs 7 statistical algorithms (e.g., Seasonal Naive, Linear Trend, Regression). It backtests them against your history and scientifically selects the winner with the…

Source: https://n8n.io/workflows/12289/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

This workflow was born out of a very real problem.

Output Parser Structured, OpenAI Chat, Memory Buffer Window +11
AI & RAG

This n8n-powered workflow automates the entire lifecycle of real estate lead intake, qualification, routing, assignment, and reporting across multiple channels. It brings WhatsApp inquiries and websit

Gmail, Typeform Trigger, WhatsApp Trigger +8
AI & RAG

kisisel asistan. Uses toolWorkflow, toolHttpRequest, toolCalculator, toolThink. Scheduled trigger; 43 nodes.

Tool Workflow, Tool Http Request, Tool Calculator +15
AI & RAG

This workflow automates end-to-end sustainability lifecycle management for corporate sustainability teams, ESG governance officers, and circular economy programme leads. It addresses the challenge of

Form Trigger, Agent, OpenAI Chat +11
AI & RAG

This workflow automates end-to-end ESG (Environmental, Social, and Governance) sustainability reporting for enterprise sustainability teams, compliance officers, and green governance leads. It solves

Agent, OpenAI Chat, Output Parser Structured +12