{
  "name": "No customer waits more than an hour: daily digest of unanswered WhatsApp chats",
  "nodes": [
    {
      "parameters": {
        "content": "What 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 limitations:\n1. `GET https://developers.wasync.app/api/v1/messages` returns each message's connection, direction, text and timestamp, but not the counterparty's phone number (see the Message object in the OpenAPI spec). 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 inbound Webhook payload into your own store as messages arrive, and join on that instead.\n2. `GET /messages` is cursor-paginated (max 100 per page) and this template fetches only the single most recent page per connection -- it does not follow `nextCursor`. On a very busy connection, an inbound message could in theory fall off the page before this workflow runs; for most support volumes, one page covers the whole waiting window comfortably.\n\nSetup:\n1. Create an API key at https://developers.wasync.app/keys (scopes: whatsapp.read).\n2. In n8n, go to Credentials -> New -> Header Auth. Name the credential \"WASync API key\". Set its Name field to `Authorization` and its Value field to `Bearer wsk_live_<your key>`.\n3. Open the \"Settings\" node at the top of this workflow and fill in your own values. Never commit real portal URLs, tokens, connection ids, 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\nThis template uses only built-in n8n nodes, so it works on n8n Cloud; if you self-host, you can also use our community node n8n-nodes-wasync.\n\nDocs: https://docs.wasync.app/docs/guides/n8n?utm_source=n8n&utm_medium=template&utm_campaign=t5-http",
        "height": 680,
        "width": 560
      },
      "name": "Read Me First",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -160
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 18,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "name": "Daily 18:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        460
      ]
    },
    {
      "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,
        700
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "https://developers.wasync.app/api/v1/connections",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {}
      },
      "name": "List Connections",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        260,
        700
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "WASync API key"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// GET /connections returns one item shaped { connections: [...] }.\n// Turn each connection into its own item so the next HTTP Request node fetches messages once per connection.\nconst conns = $input.first().json.connections || [];\nreturn conns.map((c) => ({ json: c }));"
      },
      "name": "Split Connections",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        520,
        700
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "https://developers.wasync.app/api/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {},
        "sendQuery": true,
        "specifyQuery": "keypair",
        "queryParameters": {
          "parameters": [
            {
              "name": "connectionId",
              "value": "={{ $json.id }}"
            },
            {
              "name": "limit",
              "value": "100"
            }
          ]
        }
      },
      "name": "Get Many Messages",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        780,
        700
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "WASync API key"
        }
      }
    },
    {
      "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 = $('Split Connections').all().map((i) => i.json);\nconst labelById = Object.fromEntries(\n  connections.map((c) => [c.id, c.label || c.phoneNumber || c.id])\n);\n\n// Each input item is one connection's message page: { messages: [...], nextCursor, hasMore }.\nconst byConnection = {};\nfor (const item of $input.all()) {\n  const page = item.json.messages || [];\n  for (const m of page) {\n    if (!m.connectionId) continue;\n    (byConnection[m.connectionId] = byConnection[m.connectionId] || []).push(m);\n  }\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": [
        1040,
        700
      ]
    },
    {
      "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": [
        1300,
        700
      ],
      "credentials": {
        "smtp": {
          "name": "SMTP account"
        }
      }
    }
  ],
  "connections": {
    "Daily 18:00": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "List Connections",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List Connections": {
      "main": [
        [
          {
            "node": "Split Connections",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Connections": {
      "main": [
        [
          {
            "node": "Get Many Messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Many 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"
  }
}
