{
  "name": "No customer waits more than an hour: daily digest of unanswered WhatsApp chats",
  "nodes": [
    {
      "parameters": {
        "content": "## No customer waits more than an hour: daily digest of unanswered WhatsApp chats\n\nWhat you get: one message (or email) every evening listing the WhatsApp conversations still waiting for a reply, and for how long.\nWhat it replaces: scrolling through the inbox to find who was forgotten.\nSetup time: about 10 minutes.\n\nA daily list of who is still waiting — before they stop waiting.\n\nWorks with a normal WhatsApp number — no Meta Business account needed.\n\nKnown limitation: WASync's message-history endpoint returns each message's connection, direction, text and timestamp, but not the counterparty's phone number (see the WASync API docs -- Message object). So this digest can only detect \"this WhatsApp number has an unanswered inbound message\", not which specific contact is waiting, per connection. If you need per-contact detail, capture `message.from` from the WASync Trigger's webhook payload into your own store as messages arrive, and join on that instead.\n\nSetup:\n1. Install the community node: n8n Settings -> Community Nodes -> npm package `n8n-nodes-wasync`.\n2. Create a free WASync account at https://developers.wasync.app/register, connect a WhatsApp number, then create an API key at https://developers.wasync.app/keys.\n3. In n8n, add a new \"WASync API\" credential using that key.\n4. Open the \"Settings\" node at the top of this workflow and fill in your own values. Never commit real portal URLs, tokens, or phone numbers.\n\nAlso add an SMTP credential in n8n (Credentials -> New -> Send Email (SMTP)) for the \"Send Digest Email\" node, and set your recipient address in the Settings node.\n\nDocs: https://docs.wasync.app/docs/guides/n8n?utm_source=n8n&utm_medium=template&utm_campaign=t5",
        "height": 660,
        "width": 1560
      },
      "name": "Read Me First",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -140,
        -740
      ]
    },
    {
      "parameters": {
        "content": "**① Trigger**\nRuns once a day at 18:00 in the workflow's timezone.",
        "height": 322,
        "width": 280,
        "color": 7
      },
      "name": "① Trigger",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -140,
        426
      ]
    },
    {
      "parameters": {
        "content": "**② Collect waiting chats**\nLists your WhatsApp connections, reads their recent messages,\nand flags the ones whose last inbound message is older than\nthe threshold in Settings.",
        "height": 322,
        "width": 920,
        "color": 7
      },
      "name": "② Collect waiting chats",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        180,
        426
      ]
    },
    {
      "parameters": {
        "content": "**③ Send the digest**\nOne HTML email: who is still waiting, and for how long.",
        "height": 322,
        "width": 280,
        "color": 7
      },
      "name": "③ Send the digest",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1140,
        426
      ]
    },
    {
      "parameters": {
        "content": "**Settings — fill these in**\nPlaceholders only — replace every value here before you activate the\nworkflow. Nothing else in the workflow needs editing.\nFields: digestRecipientEmail, digestFromEmail, unansweredThresholdMinutes.",
        "height": 366,
        "width": 600,
        "color": 7
      },
      "name": "Settings — fill these in",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -140,
        0
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 18,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "name": "Run Daily at 18:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        628
      ]
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "Settings-0",
              "name": "digestRecipientEmail",
              "value": "ops@yourcompany.com",
              "type": "string"
            },
            {
              "id": "Settings-1",
              "name": "digestFromEmail",
              "value": "wasync-digest@yourcompany.com",
              "type": "string"
            },
            {
              "id": "Settings-2",
              "name": "unansweredThresholdMinutes",
              "value": 120,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        0,
        246
      ]
    },
    {
      "parameters": {
        "resource": "connection",
        "operation": "list"
      },
      "name": "List WhatsApp Connections",
      "type": "n8n-nodes-wasync.wasync",
      "typeVersion": 1,
      "position": [
        320,
        628
      ],
      "credentials": {
        "wasyncApi": {
          "name": "WASync account"
        }
      }
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "getAll",
        "connectionId": "={{ $json.id }}",
        "returnAll": true
      },
      "name": "List Recent Messages",
      "type": "n8n-nodes-wasync.wasync",
      "typeVersion": 1,
      "position": [
        640,
        628
      ],
      "credentials": {
        "wasyncApi": {
          "name": "WASync account"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// NOTE: the WASync Message object does not include the counterparty's phone number, only\n// connectionId / direction / text / status / createdAt (see the WASync OpenAPI spec). So we can only\n// flag a WHOLE CONNECTION as \"waiting\" when its most recent message is inbound and older than the\n// threshold -- not identify the specific contact. See the sticky note for a per-contact alternative.\nconst thresholdMinutes = Number($('Settings').item.json.unansweredThresholdMinutes) || 120;\nconst thresholdMs = thresholdMinutes * 60 * 1000;\nconst now = Date.now();\n\nconst connections = $('List WhatsApp Connections').all().map((i) => i.json);\nconst labelById = Object.fromEntries(\n  connections.map((c) => [c.id, c.label || c.phoneNumber || c.id])\n);\n\nconst byConnection = {};\nfor (const item of $input.all()) {\n  const m = item.json;\n  if (!m.connectionId) continue;\n  (byConnection[m.connectionId] = byConnection[m.connectionId] || []).push(m);\n}\n\nfunction escapeHtml(s) {\n  return String(s == null ? '' : s).replace(/[&<>\"']/g, (c) => (\n    { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]\n  ));\n}\n\nconst waiting = [];\nfor (const [connectionId, msgs] of Object.entries(byConnection)) {\n  msgs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));\n  const last = msgs[0];\n  if (!last || last.direction !== 'incoming') continue;\n  const waitedMs = now - new Date(last.createdAt).getTime();\n  if (waitedMs < thresholdMs) continue;\n  waiting.push({\n    connectionLabel: labelById[connectionId] || connectionId,\n    lastMessage: last.text || '(media message)',\n    minutesWaiting: Math.round(waitedMs / 60000),\n  });\n}\nwaiting.sort((a, b) => b.minutesWaiting - a.minutesWaiting);\n\nconst rows = waiting\n  .map((w) => `<tr><td>${escapeHtml(w.connectionLabel)}</td><td>${escapeHtml(w.lastMessage)}</td><td>${w.minutesWaiting}</td></tr>`)\n  .join('');\n\nconst html = `<table border=\"1\" cellpadding=\"6\" style=\"border-collapse:collapse;font-family:sans-serif\"><thead><tr><th>WhatsApp number</th><th>Last message</th><th>Minutes waiting</th></tr></thead><tbody>${rows || '<tr><td colspan=\"3\">No unanswered conversations today.</td></tr>'}</tbody></table>`;\n\nreturn [{ json: { html, count: waiting.length } }];"
      },
      "name": "Find Unanswered Conversations",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        628
      ]
    },
    {
      "parameters": {
        "fromEmail": "={{ $('Settings').item.json.digestFromEmail }}",
        "toEmail": "={{ $('Settings').item.json.digestRecipientEmail }}",
        "subject": "=WhatsApp daily digest -- {{ $json.count }} unanswered conversation(s)",
        "emailFormat": "html",
        "message": "={{ $json.html }}",
        "options": {}
      },
      "name": "Send Digest Email",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1280,
        628
      ],
      "credentials": {
        "smtp": {
          "name": "SMTP account"
        }
      }
    }
  ],
  "connections": {
    "Run Daily at 18:00": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "List WhatsApp Connections",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List WhatsApp Connections": {
      "main": [
        [
          {
            "node": "List Recent Messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List Recent Messages": {
      "main": [
        [
          {
            "node": "Find Unanswered Conversations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Unanswered Conversations": {
      "main": [
        [
          {
            "node": "Send Digest Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
