AutomationFlowsSlack & Telegram › Analyze Woocommerce Category Sales Over Time with Airtable and Slack

Analyze Woocommerce Category Sales Over Time with Airtable and Slack

ByWeblineIndia @weblineindia on n8n.io

This workflow automatically analyzes sales data by product category, compares performance across time periods (daily, weekly or monthly), stores structured results in Airtable and sends a clear summary to Slack for quick decision-making.

Cron / scheduled trigger★★★★☆ complexity21 nodesWooCommerceAirtableSlack
Slack & Telegram Trigger: Cron / scheduled Nodes: 21 Complexity: ★★★★☆ Added:

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

This workflow follows the Airtable → Slack 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": "r1UbQa3bXqbHTzdi",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Automated Category Sales Performance Report",
  "tags": [],
  "nodes": [
    {
      "id": "f938bd6b-2b48-4b3c-9aeb-bda8b928fe0c",
      "name": "Category Map",
      "type": "n8n-nodes-base.code",
      "position": [
        -32,
        176
      ],
      "parameters": {
        "jsCode": "const map = {};\n\nfor (const item of items) {\n  const p = item.json;\n\n  if (!p.id || !p.categories?.length) continue;\n\n  map[p.id] = {\n    category_id: p.categories[0].id,\n    category_name: p.categories[0].name\n  };\n}\n\n// Fees bucket\nmap[\"fee\"] = {\n  category_id: 99,\n  category_name: \"Fees\"\n};\n\nreturn [{ json: map }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "885fb92a-a373-4630-a9be-6f4087f743bb",
      "name": "Run On Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1024,
        -16
      ],
      "parameters": {
        "rule": {
          "interval": [
            {}
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "5a8176d7-9d22-4a76-8c93-b34f249c78f2",
      "name": "Report Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        -768,
        -16
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "e83b0e89-3aa5-4e07-82e1-414e4d548dad",
              "name": "granularity",
              "type": "string",
              "value": "days"
            },
            {
              "id": "084d2db4-d7f9-469b-8142-10747817b1f0",
              "name": "top_categories_limit",
              "type": "number",
              "value": 8
            },
            {
              "id": "4b5de47e-36bb-496b-a821-5fe6abb9dd00",
              "name": "top_performer_pct",
              "type": "number",
              "value": 10
            },
            {
              "id": "f61c5995-3ea2-419b-8db6-d28e3f33a8d3",
              "name": "momentum_pct",
              "type": "number",
              "value": 30
            },
            {
              "id": "6bc07124-26c7-4b2a-a05b-728fdd139f2c",
              "name": "steady_band",
              "type": "number",
              "value": 10
            },
            {
              "id": "81b2486b-abcf-41d8-881d-767d93602bba",
              "name": "at_risk_growth_pct",
              "type": "number",
              "value": -15
            },
            {
              "id": "77ce5d9a-823b-4afa-b5d9-2a86d05d4ed6",
              "name": "discontinue_growth_pct",
              "type": "number",
              "value": -25
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "f9885621-1e1c-45ed-8ac2-be22dc333199",
      "name": "Calculate Reporting Periods",
      "type": "n8n-nodes-base.code",
      "position": [
        -592,
        -16
      ],
      "parameters": {
        "jsCode": "// Read inputs\nconst granularity = ($json.granularity || 'days').toLowerCase();\nconst now = new Date();\n\nlet period_start, period_end;\nlet prev_period_start, prev_period_end;\n\n// Helper: start of today (UTC \u2013 morning)\nconst startOfTodayUTC = new Date(Date.UTC(\n  now.getUTCFullYear(),\n  now.getUTCMonth(),\n  now.getUTCDate(),\n  0, 0, 0\n));\n\n// DAILY \u2013 TEST MODE (UPDATED)\n// DAILY \u2013 SAFE TEST MODE (Woo-friendly)\nif (granularity === 'days') {\n\n  // Current Period = Yesterday 00:00 UTC \u2192 now\n  period_end = now;\n\n  period_start = new Date(Date.UTC(\n    now.getUTCFullYear(),\n    now.getUTCMonth(),\n    now.getUTCDate() - 1, //  buffer\n    0, 0, 0\n  ));\n\n  // Previous Period = 6 days before current start\n  prev_period_end = new Date(period_start);\n  prev_period_start = new Date(prev_period_end);\n  prev_period_start.setUTCDate(prev_period_start.getUTCDate() - 6);\n}\n\n\n// WEEKLY \u2013 calendar weeks\nelse if (granularity === 'weekly') {\n  const day = now.getUTCDay(); // 0 = Sunday\n  const diffToMonday = (day + 6) % 7;\n\n  period_end = new Date(Date.UTC(\n    now.getUTCFullYear(),\n    now.getUTCMonth(),\n    now.getUTCDate() - diffToMonday,\n    0, 0, 0\n  ));\n\n  period_start = new Date(period_end);\n  period_start.setUTCDate(period_start.getUTCDate() - 7);\n\n  prev_period_end = new Date(period_start);\n  prev_period_start = new Date(prev_period_end);\n  prev_period_start.setUTCDate(prev_period_start.getUTCDate() - 7);\n}\n\n//  MONTHLY \u2013 calendar months\nelse if (granularity === 'monthly') {\n  period_end = new Date(Date.UTC(\n    now.getUTCFullYear(),\n    now.getUTCMonth(),\n    1,\n    0, 0, 0\n  ));\n\n  period_start = new Date(period_end);\n  period_start.setUTCMonth(period_start.getUTCMonth() - 1);\n\n  prev_period_end = new Date(period_start);\n  prev_period_start = new Date(prev_period_end);\n  prev_period_start.setUTCMonth(prev_period_start.getUTCMonth() - 1);\n}\n\n// Safety\nelse {\n  throw new Error(`Unsupported granularity: ${granularity}`);\n}\n\n//  Output\nreturn [\n  {\n    json: {\n      granularity,\n      period_start: period_start.toISOString(),\n      period_end: period_end.toISOString(),\n      prev_period_start: prev_period_start.toISOString(),\n      prev_period_end: prev_period_end.toISOString(),\n      top_categories_limit: $json.top_categories_limit || 8\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "af7baea5-1e10-4396-8a29-97d6bc70086b",
      "name": "Fetch Orders \u2014 Current Period",
      "type": "n8n-nodes-base.wooCommerce",
      "position": [
        -256,
        -208
      ],
      "parameters": {
        "options": {
          "after": "={{ $json.period_start }}",
          "status": "completed"
        },
        "resource": "order",
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "wooCommerceApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "a854efec-4411-4216-b153-68b02026a2fc",
      "name": "Fetch Orders \u2014 Previous Period",
      "type": "n8n-nodes-base.wooCommerce",
      "position": [
        -256,
        -16
      ],
      "parameters": {
        "options": {
          "after": "={{ $json.prev_period_start }}",
          "before": "={{ $json.prev_period_end }}",
          "status": "completed"
        },
        "resource": "order",
        "operation": "getAll"
      },
      "credentials": {
        "wooCommerceApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "5cc43700-36c5-4ffd-8ae5-52f74d797c91",
      "name": "Fetch Products",
      "type": "n8n-nodes-base.wooCommerce",
      "position": [
        -256,
        176
      ],
      "parameters": {
        "options": {},
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "wooCommerceApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "de3b6e71-edd1-437d-8fca-99541c2988f9",
      "name": "Normalize Order Items \u2014 Current",
      "type": "n8n-nodes-base.code",
      "position": [
        -32,
        -208
      ],
      "parameters": {
        "jsCode": "// Initialize output array\nconst newItems = [];\n\n// Loop through all items from previous node\nfor (const item of items) {\n    const order = item.json; // actual order object\n\n    const orderInfo = {\n        order_id: order.id,\n        order_number: order.number,\n        order_total: parseFloat(order.total),\n        order_date: order.date_created,\n        currency: order.currency,\n        customer_id: order.customer_id,\n        billing_name: `${order.billing.first_name} ${order.billing.last_name}`,\n        shipping_name: `${order.shipping.first_name} ${order.shipping.last_name}`,\n    };\n\n    // Use line_items if present, else fee_lines\n    const itemsList = (order.line_items && order.line_items.length > 0)\n        ? order.line_items\n        : (order.fee_lines && order.fee_lines.length > 0 ? order.fee_lines : []);\n\n    if (itemsList.length === 0) continue; // skip empty orders\n\n    for (const itemLine of itemsList) {\n        newItems.push({\n            json: {\n                ...orderInfo,\n                item_id: itemLine.id,\n                item_name: itemLine.name,\n                item_qty: itemLine.quantity || 1,\n                item_total: parseFloat(itemLine.total || itemLine.amount || 0),\n                item_price: parseFloat(itemLine.price || itemLine.amount || 0),\n                item_type: (order.line_items && order.line_items.length > 0) ? 'product' : 'fee'\n            }\n        });\n    }\n}\n\nreturn newItems;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "865c0cc0-43a8-46bc-b729-bc17c8eeb05f",
      "name": "Normalize Order Items \u2014 Previous",
      "type": "n8n-nodes-base.code",
      "position": [
        -32,
        -16
      ],
      "parameters": {
        "jsCode": "// Initialize output array\nconst newItems = [];\n\n// Loop through all items from previous node\nfor (const item of items) {\n    const order = item.json; // actual order object\n\n    const orderInfo = {\n        order_id: order.id,\n        order_number: order.number,\n        order_total: parseFloat(order.total),\n        order_date: order.date_created,\n        currency: order.currency,\n        customer_id: order.customer_id,\n        billing_name: `${order.billing.first_name} ${order.billing.last_name}`,\n        shipping_name: `${order.shipping.first_name} ${order.shipping.last_name}`,\n    };\n\n    // Use line_items if present, else fee_lines\n    const itemsList = (order.line_items && order.line_items.length > 0)\n        ? order.line_items\n        : (order.fee_lines && order.fee_lines.length > 0 ? order.fee_lines : []);\n\n    if (itemsList.length === 0) continue; // skip empty orders\n\n    for (const itemLine of itemsList) {\n        newItems.push({\n            json: {\n                ...orderInfo,\n                item_id: itemLine.id,\n                item_name: itemLine.name,\n                item_qty: itemLine.quantity || 1,\n                item_total: parseFloat(itemLine.total || itemLine.amount || 0),\n                item_price: parseFloat(itemLine.price || itemLine.amount || 0),\n                item_type: (order.line_items && order.line_items.length > 0) ? 'product' : 'fee'\n            }\n        });\n    }\n}\n\nreturn newItems;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "b79ad266-8618-40a7-b70f-ca86d27f04fd",
      "name": "Combine Orders & Products",
      "type": "n8n-nodes-base.merge",
      "position": [
        288,
        -32
      ],
      "parameters": {
        "numberInputs": 3
      },
      "typeVersion": 3.2
    },
    {
      "id": "23d57db4-1d00-4cf8-bd67-c732d55f7458",
      "name": "Aggregate Sales by Category",
      "type": "n8n-nodes-base.code",
      "position": [
        544,
        -16
      ],
      "parameters": {
        "jsCode": "/* ======================================\n   INPUTS\n   ====================================== */\n\n// Get current and previous normalized rows\nconst currentRows = $items(\"Normalize Order Items \u2014 Current\").map(i => i.json);\nconst previousRows = $items(\"Normalize Order Items \u2014 Previous\").map(i => i.json);\n\n// Get Product \u2192 Category map if exists (optional)\nconst productCategoryMapInput = $items(\"Category Map\")?.[0]?.json || {};\n\n/* ======================================\n   BUILD DYNAMIC PRODUCT \u2192 CATEGORY MAP\n   ====================================== */\n\nconst productCategoryMap = { ...productCategoryMapInput };\n\n// Scan current and previous rows for products and add unmapped ones dynamically\nfor (const row of [...currentRows, ...previousRows]) {\n  if (row.item_type === \"product\" && !productCategoryMap[row.item_id]) {\n    // Dynamically create category for unmapped product\n    // Replace this with actual logic to fetch from WooCommerce if needed\n    // For now, we use \"Unmapped\" for any missing product\n    productCategoryMap[row.item_id] = {\n      category_id: row.item_id, // temporary: use product_id as category_id\n      category_name: row.item_name // temporary: use product_name as category_name\n    };\n  }\n}\n\n// Always include fees bucket\nproductCategoryMap[\"fee\"] = { category_id: 99, category_name: \"Fees\" };\n\n/* ======================================\n   HELPER: AGGREGATE ROWS BY CATEGORY\n   ====================================== */\nfunction aggregate(rows) {\n  const agg = {};\n\n  for (const r of rows) {\n    const key = r.item_type === \"fee\" ? \"fee\" : r.item_id;\n    const cat = productCategoryMap[key] || { category_id: \"unmapped\", category_name: \"Unmapped\" };\n    const cid = String(cat.category_id);\n\n    if (!agg[cid]) {\n      agg[cid] = {\n        category_id: cid,\n        category_name: cat.category_name,\n        revenue: 0,\n        units_sold: 0,\n        orders: new Set()\n      };\n    }\n\n    agg[cid].revenue += Number(r.item_total || 0);\n    agg[cid].units_sold += Number(r.item_qty || 0);\n    agg[cid].orders.add(r.order_id);\n  }\n\n  // Convert orders Set \u2192 number\n  for (const c of Object.values(agg)) {\n    c.orders = c.orders.size;\n  }\n\n  return agg;\n}\n\n/* ======================================\n   AGGREGATE CURRENT & PREVIOUS\n   ====================================== */\nconst curAgg = aggregate(currentRows);\nconst prevAgg = aggregate(previousRows);\n\n// Combine all category IDs\nconst allCategoryIds = new Set([...Object.keys(curAgg), ...Object.keys(prevAgg)]);\n\n// Total current revenue for share_of_total\nconst totalCurrentRevenue = Object.values(curAgg).reduce((s, c) => s + c.revenue, 0);\n\n/* ======================================\n   BUILD FINAL OUTPUT\n   ====================================== */\nconst result = [];\n\nfor (const cid of allCategoryIds) {\n  const cur = curAgg[cid] || { revenue: 0, units_sold: 0, orders: 0, category_name: prevAgg[cid]?.category_name || \"Unmapped\" };\n  const prev = prevAgg[cid] || { revenue: 0, units_sold: 0, orders: 0, category_name: cur.category_name };\n\n  const abs_change = cur.revenue - prev.revenue;\n  const pct_change = prev.revenue === 0 ? \"n/a\" : ((abs_change / prev.revenue) * 100).toFixed(2);\n  const share_of_total = totalCurrentRevenue === 0 ? \"0.00\" : ((cur.revenue / totalCurrentRevenue) * 100).toFixed(2);\n\n  result.push({\n    category_id: cid,\n    category_name: cur.category_name,\n    revenue_current: cur.revenue,\n    revenue_prev: prev.revenue,\n    abs_change,\n    pct_change,\n    units_sold: cur.units_sold,\n    orders_current: cur.orders,\n    share_of_total\n  });\n}\n\n/* ======================================\n   RETURN ITEMS FOR N8N\n   ====================================== */\nreturn result.map(r => ({ json: r }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "8aedb373-070a-4188-886c-bf85e6071168",
      "name": "Classify Category Performance",
      "type": "n8n-nodes-base.code",
      "position": [
        736,
        -16
      ],
      "parameters": {
        "jsCode": "const categories = $items(\"Aggregate Sales by Category\").map(i => i.json);\n\n// Sort categories by revenue_current descending\ncategories.sort((a, b) => b.revenue_current - a.revenue_current);\n\n// Only categories with positive current revenue\nconst positiveRevenueCategories = categories.filter(c => c.revenue_current > 0);\nconst totalPositive = positiveRevenueCategories.length;\n\n// Generate rank map\nconst rankMap = {};\npositiveRevenueCategories.forEach((c, idx) => {\n    rankMap[c.category_id] = idx;\n});\n\n// Dynamic thresholds\nconst thresholds = {\n    top: 0.2,      // top 20%\n    momentum: 0.5, // top 50%\n    steady: 0.8    // top 80%\n};\n\ncategories.forEach(cat => {\n    let tag = \"\";\n    let recommended_action = \"\";\n\n    if (cat.revenue_current === 0 && cat.revenue_prev > 0) {\n        // Category had revenue before but 0 now\n        tag = \"Consider Discontinue\";\n        recommended_action = \"Investigate returns or delisting\";\n    } else if (cat.revenue_current === 0 && cat.revenue_prev === 0) {\n        // Category never sold\n        tag = \"At Risk\";\n        recommended_action = \"Promotions or marketing\";\n    } else {\n        // Category has revenue now (revenue_current > 0)\n        const rank = rankMap[cat.category_id];\n        const rankPercent = totalPositive > 1 ? (rank + 1) / totalPositive : 0;\n\n        if (rankPercent <= thresholds.top) {\n            tag = \"Top Performer\";\n            recommended_action = \"Feature on homepage\";\n        } else if (rankPercent <= thresholds.momentum) {\n            tag = \"Momentum\";\n            recommended_action = \"Investigate increasing stock\";\n        } else if (rankPercent <= thresholds.steady) {\n            tag = \"Steady\";\n            recommended_action = \"Maintain current strategy\";\n        } else {\n            tag = \"At Risk\";\n            recommended_action = \"Promotions or marketing\";\n        }\n    }\n\n    cat.tag = tag;\n    cat.recommended_action = recommended_action;\n});\n\nreturn categories.map(c => ({ json: c }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "fc6b5a47-97f3-4b45-a867-d2ba473833ca",
      "name": "Prepare Fields for Reporting",
      "type": "n8n-nodes-base.set",
      "position": [
        944,
        -16
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "72e7696b-0045-4f17-90c3-e41b4e3dd8a2",
              "name": "category_id",
              "type": "string",
              "value": "={{ $json.category_id }}"
            },
            {
              "id": "83ef1840-c74c-4dd8-9d1f-843696194a1d",
              "name": "revenue_current",
              "type": "number",
              "value": "={{ $json.revenue_current }}"
            },
            {
              "id": "cdd10e1b-7030-4053-9824-7d9ae1c0d792",
              "name": "revenue_prev",
              "type": "number",
              "value": "={{ $json.revenue_prev }}"
            },
            {
              "id": "6b10bdd7-7a77-4d6a-8d1e-e098018402b3",
              "name": "units_sold",
              "type": "number",
              "value": "={{ $json.units_sold }}"
            },
            {
              "id": "f0208637-1721-4008-803d-fe7ff7a9a4e9",
              "name": "share_of_total",
              "type": "string",
              "value": "={{ $json.share_of_total }}"
            },
            {
              "id": "9e65e3dc-0b3e-4243-8382-babc08dbc799",
              "name": "tag",
              "type": "string",
              "value": "={{ $json.tag }}"
            },
            {
              "id": "75acd458-0373-40dc-b99a-7c5761c54901",
              "name": "recommended_action",
              "type": "string",
              "value": "={{ $json.recommended_action }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "5151087f-6f04-4d13-955e-4f8fe67d8b8f",
      "name": "Save Results to Airtable",
      "type": "n8n-nodes-base.airtable",
      "position": [
        1184,
        -16
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "appF2iYPgVqqyXDC1",
          "cachedResultUrl": "https://airtable.com/appF2iYPgVqqyXDC1",
          "cachedResultName": "n8n Demo"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "tbloZJJZU0wCvi2qd",
          "cachedResultUrl": "https://airtable.com/appF2iYPgVqqyXDC1/tbloZJJZU0wCvi2qd",
          "cachedResultName": "Category Performance"
        },
        "columns": {
          "value": {
            "Tag": "={{ $json.tag }}",
            "Share": "={{ $json.share_of_total }}",
            "Units": "={{ $json.units_sold }}",
            "Action ": "={{ $json.recommended_action }}",
            "Category Id": "={{ $json.category_id }}",
            "Current Revenue": "={{ $json.revenue_current }}",
            "Previous Revenue": "={{ $json.revenue_prev }}"
          },
          "schema": [
            {
              "id": "Category Id",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Category Id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Current Revenue",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Current Revenue",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Previous Revenue",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Previous Revenue",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Units",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Units",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Share",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Share",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Tag",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Tag",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Action ",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Action ",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "create"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "7735c6ef-f872-487b-8f38-f377452dd0ac",
      "name": "Build Slack Summary",
      "type": "n8n-nodes-base.code",
      "position": [
        1424,
        -16
      ],
      "parameters": {
        "jsCode": "// Airtable rows come as: item.fields\nconst rows = items.map(i => i.json.fields);\n\n// Helpers\nconst formatMoney = (v) =>\n  typeof v === \"number\" ? `\u20b9${v.toLocaleString(\"en-IN\")}` : \"\u20b90\";\n\n// Split categories\nconst top = rows\n  .filter(r => r[\"Current Revenue\"] > 0)\n  .sort((a, b) => b[\"Current Revenue\"] - a[\"Current Revenue\"])\n  .slice(0, 5);\n\nconst risk = rows.filter(r =>\n  [\"At Risk\", \"Consider Discontinue\"].includes(r[\"Tag\"])\n);\n\n// Build text\nlet text = `*Category Performance Summary*\\n\\n`;\n\ntext += `*Top Categories*\\n`;\nif (top.length === 0) {\n  text += `\u2022 No sales in current period\\n`;\n} else {\n  for (const r of top) {\n    const prev = r[\"Previous Revenue\"];\n    const curr = r[\"Current Revenue\"];\n\n    const pct =\n      prev > 0\n        ? `${(((curr - prev) / prev) * 100).toFixed(1)}%`\n        : \"n/a\";\n\n    text += `\u2022 *Category ${r[\"Category Id\"]}* \u2014 ${formatMoney(curr)} (${pct}) | ${r[\"Tag\"]}\\n`;\n  }\n}\n\nif (risk.length) {\n  text += `\\n *Needs Attention*\\n`;\n  for (const r of risk) {\n    text += `\u2022 *Category ${r[\"Category Id\"]}* \u2014 ${formatMoney(\n      r[\"Current Revenue\"]\n    )} | ${r[\"Action \"]}\\n`;\n  }\n}\n\ntext += `\\n Full report available in Airtable.`;\n\nreturn [{ json: { text } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2173ef7a-fbf2-4d79-afa2-c9b6ff4d975c",
      "name": "Send Report to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        1664,
        -16
      ],
      "parameters": {
        "text": "={{ $json.text }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C09S57E2JQ2",
          "cachedResultName": "n8n"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "3ccb02a4-6ad4-4765-94c9-bda0ba405603",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1088,
        -112
      ],
      "parameters": {
        "color": 7,
        "height": 240,
        "content": "Starts the workflow automatically on a fixed schedule (daily / weekly)."
      },
      "typeVersion": 1
    },
    {
      "id": "e7ef5742-3f3d-4a04-9529-e57addc8ed2b",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -816,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 368,
        "height": 272,
        "content": "Sets the report rules and automatically calculates current and previous date ranges for sales comparison."
      },
      "typeVersion": 1
    },
    {
      "id": "01bd5059-3216-40d5-8edb-41dc1ac5dd90",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -416,
        -432
      ],
      "parameters": {
        "color": 7,
        "width": 848,
        "height": 784,
        "content": "## Fetch & Normalize \n\n**Fetch Orders \u2014 Current Period:** Fetches completed WooCommerce orders from the current time period.\n**Fetch Orders \u2014 Previous Period:** Fetches completed WooCommerce orders from the previous comparison period.\n**Fetch Products:** Loads product and category details from WooCommerce.\n**Normalize Order Items \u2014 Current:** Breaks current orders into individual product-level sales rows.\n**Normalize Order Items \u2014 Previous:** Breaks previous orders into individual product-level sales rows. \n**Category Map:** Maps each product to its category for accurate category reporting.\n**Combine Orders & Products:** Combines current and previous sales data into a single stream."
      },
      "typeVersion": 1
    },
    {
      "id": "22726a24-2718-4616-8324-382065a42298",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        480,
        -288
      ],
      "parameters": {
        "color": 7,
        "width": 1376,
        "height": 496,
        "content": "## Category Performance Analysis & Reporting \n\n**Aggregate Sales by Category:** Calculates total revenue, units sold and orders per category.\n**Classify Category Performance:** Labels categories as Top Performer, Momentum, Steady or At Risk.\n**Prepare Fields for Reporting:** Formats and selects final fields needed for reports and storage.\n**Save Results to Airtable:** Saves category performance results to Airtable for tracking and history.\n**Build Slack Summary:** Creates a short, readable summary of category performance.\n**Send Report to Slack:** Sends the final performance summary to the Slack channel."
      },
      "typeVersion": 1
    },
    {
      "id": "63bd8b58-2c0a-48f9-9b8a-e53837882118",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1776,
        -864
      ],
      "parameters": {
        "width": 576,
        "height": 864,
        "content": "# How it Works\n\n### This workflow automatically analyzes sales data by product category and compares current performance with the previous period (weekly or monthly).\n### It pulls orders for two time periods (current and previous), groups sales by category and calculates key metrics such as revenue, units sold and share of total sales.\n### Based on these results, each category is classified (for example: Top Performer, Steady or Needs Attention) along with a recommended action.\n ### The final results are saved to Airtable for tracking, reporting and historical comparison.\n### A short, easy-to-read summary is generated and sent to Slack, allowing stakeholders to quickly see what\u2019s performing well and what needs attention\u2014without opening dashboards or detailed reports.\n### This workflow runs automatically and ensures everyone receives consistent, up-to-date performance insights.\n\n\n# Setup steps\n\n**1.** Configure the date range (current period and previous days) in the date range node.\n\n**2.** Connect your order source (e.g., WooCommerce or API) for both current and previous periods.\n\n**3.** Make sure categories and revenue fields match your order data structure.\n\n**4.** Connect Airtable and map fields for category metrics and actions.\n\n**5.** Connect Slack and select the channel where summaries should be posted.\n\n**6.** Test once with recent data, then activate the workflow."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "40a78233-f5fa-4cdc-8093-6d4276c84deb",
  "connections": {
    "Category Map": {
      "main": [
        [
          {
            "node": "Combine Orders & Products",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Fetch Products": {
      "main": [
        [
          {
            "node": "Category Map",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run On Schedule": {
      "main": [
        [
          {
            "node": "Report Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Slack Summary": {
      "main": [
        [
          {
            "node": "Send Report to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Report Configuration": {
      "main": [
        [
          {
            "node": "Calculate Reporting Periods",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Results to Airtable": {
      "main": [
        [
          {
            "node": "Build Slack Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Orders & Products": {
      "main": [
        [
          {
            "node": "Aggregate Sales by Category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Sales by Category": {
      "main": [
        [
          {
            "node": "Classify Category Performance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Reporting Periods": {
      "main": [
        [
          {
            "node": "Fetch Orders \u2014 Previous Period",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Orders \u2014 Current Period",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Fields for Reporting": {
      "main": [
        [
          {
            "node": "Save Results to Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Category Performance": {
      "main": [
        [
          {
            "node": "Prepare Fields for Reporting",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Orders \u2014 Current Period": {
      "main": [
        [
          {
            "node": "Normalize Order Items \u2014 Current",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Orders \u2014 Previous Period": {
      "main": [
        [
          {
            "node": "Normalize Order Items \u2014 Previous",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Order Items \u2014 Current": {
      "main": [
        [
          {
            "node": "Combine Orders & Products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Order Items \u2014 Previous": {
      "main": [
        [
          {
            "node": "Combine Orders & Products",
            "type": "main",
            "index": 1
          }
        ]
      ]
    }
  }
}

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

This workflow automatically analyzes sales data by product category, compares performance across time periods (daily, weekly or monthly), stores structured results in Airtable and sends a clear summary to Slack for quick decision-making.

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

More Slack & Telegram workflows → · Browse all categories →

Related workflows

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

Slack & Telegram

This weekly workflow automatically identifies new ranked keywords for your domain within Google’s top 10 results without manual SERP monitoring. On each run, the workflow fetches the latest ranking an

N8N Nodes Dataforseo, Airtable, Slack
Slack & Telegram

This workflow exports every table in a base as its own CSV, saves the files in a time-stamped folder in Amazon S3, pings you on Slack, and optionally prunes older copies. You get an automated weekly b

Airtable, AWS S3, Slack
Slack & Telegram

This template provides a fully automated system for monitoring news on any topic you choose. It leverages Linkup's AI-powered web search to find recent, relevant articles, extracts key information lik

HTTP Request, Airtable, Slack
Slack & Telegram

This n8n workflow monitors WooCommerce refund activity to detect unusual spikes in product returns at the SKU level. It compares return volumes across rolling 24-hour windows, alerts teams in Slack wh

Slack, Airtable, HTTP Request
Slack & Telegram

This workflow automatically collects WooCommerce sales data every day, calculates key sales metrics, sends a clean summary to Slack and logs the same data into Google Sheets for historical tracking. I

WooCommerce, Slack, Google Sheets