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 →
{
"name": "W5 - OUT WhatsApp Sender (Meta Cloud API)",
"active": false,
"settings": {
"executionTimeout": 120,
"saveExecutionProgress": true,
"saveManualExecutions": true
},
"nodes": [
{
"parameters": {},
"id": "2679a5a9-fe7a-44b0-ad65-f17fa329f6e0",
"name": "IN - From CORE",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1,
"position": [
-2400,
0
]
},
{
"parameters": {
"language": "javascript",
"jsCode": "/**\n * P0-07 + P1-03 + P1-06: Generate Outbox Key and prepare message envelope\n * Supports both sync (direct send) and async (queue for worker) modes\n * P1-06: Propagate correlation_id for end-to-end tracing\n */\nconst crypto = require('crypto');\nconst payload = $json;\nconst timestamp = new Date().toISOString();\n\n// P1-06: Get correlation_id from CORE (propagated through _timing)\nconst correlationId = payload._timing?.correlation_id || payload.correlation_id || crypto.randomUUID();\n\n// P1-03: Check if async mode is enabled\nconst asyncEnabled = (($env.OUTBOX_ASYNC_ENABLED || 'false').toString().toLowerCase() === 'true');\n\n// Generate unique outbox message ID\nconst outboxMsgId = payload.outboxMsgId || payload.msgId || crypto.randomUUID();\nconst outboxKey = `ralphe:outbox:wa:${outboxMsgId}`;\nconst outboxTtl = parseInt($env.OUTBOX_REDIS_TTL_SEC || '604800', 10); // 7 days default\n\n// Prepare outbox entry with correlation_id\nconst outboxEntry = {\n channel: 'whatsapp',\n correlation_id: correlationId,\n outboxMsgId,\n outboxKey,\n createdAt: timestamp,\n payload: {\n userId: payload.userId,\n restaurantId: payload.restaurantId,\n replyText: payload.replyText,\n buttons: payload.buttons,\n attachments: payload.attachments,\n templateName: payload.templateName,\n templateParams: payload.templateParams,\n locale: payload.locale\n },\n status: 'pending',\n attempts: 0\n};\n\nreturn [{\n json: {\n ...payload,\n _timing: { ...payload._timing, correlation_id: correlationId },\n _outbox: {\n key: outboxKey,\n msgId: outboxMsgId,\n ttl: outboxTtl,\n entry: JSON.stringify(outboxEntry),\n asyncEnabled,\n correlation_id: correlationId\n }\n }\n}];\n"
},
"id": "outbox-prepare-wa",
"name": "B0 - Prepare Outbox",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2150,
0
]
},
{
"parameters": {
"operation": "set",
"key": "={{$json._outbox.key}}",
"value": "={{$json._outbox.entry}}",
"expire": true,
"ttl": "={{$json._outbox.ttl}}"
},
"id": "outbox-store-wa",
"name": "B0 - Store in Outbox",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
-1900,
0
],
"credentials": {
"redis": {
"name": "<your credential>"
}
},
"continueOnFail": true
},
{
"parameters": {
"language": "javascript",
"jsCode": "/**\n * W5 - WhatsApp Outbound Sender\n * Supports both Mock API (dev) and Meta Cloud API (production)\n * \n * Meta Cloud API format:\n * POST https://graph.facebook.com/v21.0/{PHONE_NUMBER_ID}/messages\n * Body: { messaging_product: 'whatsapp', to: 'PHONE', type: 'text', text: { body: 'MSG' } }\n * \n * Input from CORE:\n * { channel, restaurantId, userId, replyText, buttons, attachments, templateName, templateParams, locale }\n */\n\nconst payload = $json;\nconst timestamp = new Date().toISOString();\nconst cfg = payload._strapiConfig || {};\n\n// Configuration - STRAPI FIRST, ENV FALLBACK\nconst graphVersion = (cfg.graph_api_version || $env.GRAPH_API_VERSION || 'v21.0').toString().trim();\nconst sendUrl = (cfg.wa_send_url || $env.WA_SEND_URL || '').toString().trim();\nconst phoneNumberId = (cfg.whatsapp_phone_number_id || $env.WA_PHONE_NUMBER_ID || '').toString().trim();\nconst token = (cfg.whatsapp_access_token || $env.WA_API_TOKEN || '').toString().trim();\nconst maxRetries = parseInt($env.OUTBOX_MAX_ATTEMPTS || '3', 10);\nconst baseDelay = parseInt($env.OUTBOX_BASE_DELAY_SEC || '1', 10) * 1000;\nconst maxDelay = parseInt($env.OUTBOX_MAX_DELAY_SEC || '60', 10) * 1000;\n\n// Detect if using Meta Cloud API (production) or mock\nconst isMetaApi = sendUrl.includes('graph.facebook.com') || !!phoneNumberId;\nlet url = sendUrl;\n\nif (!url && phoneNumberId) {\n // Auto-construct Meta Cloud API URL\n url = `https://graph.facebook.com/${graphVersion}/${phoneNumberId}/messages`;\n}\n\nif (!url) {\n return [{ json: { \n ...payload,\n sent: false, \n error: 'WA_SEND_URL_NOT_SET',\n reason: 'WA_SEND_URL or WA_PHONE_NUMBER_ID must be configured',\n timestamp\n }}];\n}\n\nif (!token) {\n return [{ json: { \n ...payload,\n sent: false, \n error: 'WA_API_TOKEN_NOT_SET',\n reason: 'WA_API_TOKEN must be configured',\n timestamp\n }}];\n}\n\nconst recipientPhone = (payload.userId || '').toString().trim();\nif (!recipientPhone) {\n return [{ json: { \n ...payload,\n sent: false, \n error: 'RECIPIENT_MISSING',\n reason: 'userId (phone number) is required',\n timestamp\n }}];\n}\n\n// Build request body\nlet body;\nif (isMetaApi) {\n // Meta Cloud API format\n const text = (payload.replyText || '').toString().trim();\n const templateName = (payload.templateName || '').toString().trim();\n \n if (templateName) {\n // Template message (for outbound outside 24h window)\n const templateParams = Array.isArray(payload.templateParams) ? payload.templateParams : [];\n const locale = (payload.locale || 'fr').toString();\n \n body = {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n to: recipientPhone,\n type: 'template',\n template: {\n name: templateName,\n language: { code: locale === 'ar' ? 'ar' : 'fr' },\n components: templateParams.length > 0 ? [{\n type: 'body',\n parameters: templateParams.map(p => ({ type: 'text', text: String(p) }))\n }] : undefined\n }\n };\n } else if (Array.isArray(payload.buttons) && payload.buttons.length > 0) {\n // Interactive message with buttons\n body = {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n to: recipientPhone,\n type: 'interactive',\n interactive: {\n type: 'button',\n body: { text: text || 'Choose an option:' },\n action: {\n buttons: payload.buttons.slice(0, 3).map((btn, i) => ({\n type: 'reply',\n reply: {\n id: btn.id || `btn_${i}`,\n title: (btn.title || btn.label || '').slice(0, 20)\n }\n }))\n }\n }\n };\n } else {\n // Simple text message\n body = {\n messaging_product: 'whatsapp',\n recipient_type: 'individual',\n to: recipientPhone,\n type: 'text',\n text: { \n preview_url: false,\n body: text || '(empty message)'\n }\n };\n }\n} else {\n // Mock API format (dev/test)\n body = {\n channel: 'whatsapp',\n to: recipientPhone,\n restaurantId: payload.restaurantId || '',\n text: payload.replyText || '',\n buttons: Array.isArray(payload.buttons) ? payload.buttons : [],\n attachments: Array.isArray(payload.attachments) ? payload.attachments : []\n };\n}\n\n// Send request with retry logic\nlet lastError = null;\nlet response = null;\nlet finalAttempt = 0;\n\nfor (let attempt = 1; attempt <= maxRetries; attempt++) {\n finalAttempt = attempt;\n try {\n response = await $httpRequest({\n method: 'POST',\n url,\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json'\n },\n body,\n json: true,\n timeout: 30000,\n returnFullResponse: true\n });\n \n const statusCode = response.statusCode || response.status || 200;\n \n // Success\n if (statusCode >= 200 && statusCode < 300) {\n const responseBody = response.body || response.data || response;\n return [{ json: {\n ...payload,\n sent: true,\n provider: isMetaApi ? 'meta_cloud_api' : 'mock',\n messageId: responseBody?.messages?.[0]?.id || responseBody?.message_id || null,\n recipient: recipientPhone,\n timestamp,\n attempt: finalAttempt,\n response: responseBody\n }}];\n }\n \n // Rate limited - wait and retry\n if (statusCode === 429) {\n const retryAfter = parseInt(response.headers?.['retry-after'] || '60', 10);\n lastError = { statusCode, error: 'RATE_LIMITED', retryAfter };\n if (attempt < maxRetries) {\n await new Promise(r => setTimeout(r, Math.min(retryAfter * 1000, maxDelay)));\n continue;\n }\n }\n \n // Server error - retry with backoff\n if (statusCode >= 500) {\n lastError = { statusCode, error: 'SERVER_ERROR', body: response.body };\n if (attempt < maxRetries) {\n await new Promise(r => setTimeout(r, Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay)));\n continue;\n }\n }\n \n // Client error - don't retry\n if (statusCode >= 400) {\n return [{ json: {\n ...payload,\n sent: false,\n error: 'CLIENT_ERROR',\n statusCode,\n provider: isMetaApi ? 'meta_cloud_api' : 'mock',\n response: response.body || response,\n timestamp,\n attempt: finalAttempt,\n requestBody: body\n }}];\n }\n \n } catch (err) {\n lastError = { error: 'REQUEST_FAILED', message: err.message };\n if (attempt < maxRetries) {\n await new Promise(r => setTimeout(r, Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay)));\n continue;\n }\n }\n}\n\n// All retries exhausted\nreturn [{ json: {\n ...payload,\n sent: false,\n error: 'MAX_RETRIES_EXHAUSTED',\n lastError,\n provider: isMetaApi ? 'meta_cloud_api' : 'mock',\n timestamp,\n attempts: maxRetries,\n requestBody: body\n}}];\n"
},
"id": "fd8fc539-6895-4732-8b02-3cfd0a3db2ad",
"name": "OUT - Send Message",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1650,
0
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{$json.sent}}",
"operation": "isTrue"
}
]
}
},
"id": "check-send-result-wa",
"name": "B1 - Send OK?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-1400,
0
]
},
{
"parameters": {
"operation": "delete",
"key": "={{$json._outbox.key}}"
},
"id": "outbox-clear-success-wa",
"name": "B1 - Clear Outbox (Success)",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
-1150,
-100
],
"credentials": {
"redis": {
"name": "<your credential>"
}
},
"continueOnFail": true
},
{
"parameters": {
"language": "javascript",
"jsCode": "/**\n * P0-07 + P1-06: Prepare DLQ entry for failed message\n * P1-06: Include correlation_id for tracing\n */\nconst payload = $json;\nconst timestamp = new Date().toISOString();\nconst correlationId = payload._outbox?.correlation_id || payload._timing?.correlation_id || '';\n\nconst dlqEntry = {\n channel: 'whatsapp',\n correlation_id: correlationId,\n outboxMsgId: payload._outbox?.msgId,\n outboxKey: payload._outbox?.key,\n failedAt: timestamp,\n error: payload.error,\n lastError: payload.lastError,\n statusCode: payload.statusCode,\n attempts: payload.attempts || payload.attempt || 1,\n payload: {\n userId: payload.userId,\n restaurantId: payload.restaurantId,\n replyText: payload.replyText,\n buttons: payload.buttons,\n templateName: payload.templateName\n },\n requestBody: payload.requestBody,\n response: payload.response\n};\n\nreturn [{\n json: {\n ...payload,\n _dlq: {\n entry: JSON.stringify(dlqEntry),\n correlation_id: correlationId\n }\n }\n}];\n"
},
"id": "dlq-prepare-wa",
"name": "B1 - Prepare DLQ Entry",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1150,
100
]
},
{
"parameters": {
"operation": "push",
"list": "ralphe:dlq",
"messageData": "={{$json._dlq.entry}}"
},
"id": "dlq-push-wa",
"name": "B1 - Push to DLQ",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
-900,
100
],
"credentials": {
"redis": {
"name": "<your credential>"
}
},
"continueOnFail": true
},
{
"parameters": {
"operation": "delete",
"key": "={{$json._outbox.key}}"
},
"id": "outbox-clear-fail-wa",
"name": "B1 - Clear Outbox (Fail)",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
-650,
100
],
"credentials": {
"redis": {
"name": "<your credential>"
}
},
"continueOnFail": true
},
{
"parameters": {
"language": "javascript",
"jsCode": "// P0-07 + P1-06: Final result node with correlation_id\nconst payload = $json;\nconst correlationId = payload._outbox?.correlation_id || payload._timing?.correlation_id || '';\nreturn [{ json: {\n correlation_id: correlationId,\n sent: payload.sent,\n messageId: payload.messageId || null,\n recipient: payload.recipient || payload.userId,\n channel: 'whatsapp',\n outboxMsgId: payload._outbox?.msgId,\n error: payload.error || null,\n timestamp: new Date().toISOString()\n}}];\n"
},
"id": "end-success-wa",
"name": "END - Success",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-900,
-100
]
},
{
"parameters": {
"language": "javascript",
"jsCode": "// P0-07 + P1-06: Final result node (DLQ path) with correlation_id\nconst payload = $json;\nconst correlationId = payload._dlq?.correlation_id || payload._outbox?.correlation_id || payload._timing?.correlation_id || '';\nreturn [{ json: {\n correlation_id: correlationId,\n sent: false,\n messageId: null,\n recipient: payload.userId,\n channel: 'whatsapp',\n outboxMsgId: payload._outbox?.msgId,\n error: payload.error,\n dlqPushed: true,\n timestamp: new Date().toISOString()\n}}];\n"
},
"id": "end-dlq-wa",
"name": "END - DLQ",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-400,
100
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{$json._outbox.asyncEnabled}}",
"operation": "isTrue"
}
]
}
},
"id": "async-mode-check-wa",
"name": "B0 - Async Mode?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-1650,
0
]
},
{
"parameters": {
"operation": "push",
"list": "ralphe:outbox:pending",
"messageData": "={{$json._outbox.entry}}",
"tail": true
},
"id": "outbox-push-pending-wa",
"name": "B0 - LPUSH Pending Queue",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
-1400,
100
],
"credentials": {
"redis": {
"name": "<your credential>"
}
},
"continueOnFail": true
},
{
"parameters": {
"language": "javascript",
"jsCode": "// P1-03 + P1-06: Async mode - message queued for worker with correlation_id\nconst payload = $json;\nconst correlationId = payload._outbox?.correlation_id || payload._timing?.correlation_id || '';\nreturn [{ json: {\n correlation_id: correlationId,\n queued: true,\n sent: null,\n messageId: null,\n recipient: payload.userId,\n channel: 'whatsapp',\n outboxMsgId: payload._outbox?.msgId,\n mode: 'async',\n timestamp: new Date().toISOString()\n}}];\n"
},
"id": "end-queued-wa",
"name": "END - Queued",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1150,
100
]
}
],
"connections": {
"IN - From CORE": {
"main": [
[
{
"node": "B0 - Prepare Outbox",
"type": "main",
"index": 0
}
]
]
},
"B0 - Prepare Outbox": {
"main": [
[
{
"node": "B0 - Store in Outbox",
"type": "main",
"index": 0
}
]
]
},
"B0 - Store in Outbox": {
"main": [
[
{
"node": "B0 - Async Mode?",
"type": "main",
"index": 0
}
]
]
},
"B0 - Async Mode?": {
"main": [
[
{
"node": "B0 - LPUSH Pending Queue",
"type": "main",
"index": 0
}
],
[
{
"node": "OUT - Send Message",
"type": "main",
"index": 0
}
]
]
},
"B0 - LPUSH Pending Queue": {
"main": [
[
{
"node": "END - Queued",
"type": "main",
"index": 0
}
]
]
},
"OUT - Send Message": {
"main": [
[
{
"node": "B1 - Send OK?",
"type": "main",
"index": 0
}
]
]
},
"B1 - Send OK?": {
"main": [
[
{
"node": "B1 - Clear Outbox (Success)",
"type": "main",
"index": 0
}
],
[
{
"node": "B1 - Prepare DLQ Entry",
"type": "main",
"index": 0
}
]
]
},
"B1 - Clear Outbox (Success)": {
"main": [
[
{
"node": "END - Success",
"type": "main",
"index": 0
}
]
]
},
"B1 - Prepare DLQ Entry": {
"main": [
[
{
"node": "B1 - Push to DLQ",
"type": "main",
"index": 0
}
]
]
},
"B1 - Push to DLQ": {
"main": [
[
{
"node": "B1 - Clear Outbox (Fail)",
"type": "main",
"index": 0
}
]
]
},
"B1 - Clear Outbox (Fail)": {
"main": [
[
{
"node": "END - DLQ",
"type": "main",
"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.
redis
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
W5 - OUT WhatsApp Sender (Meta Cloud API). Uses executeWorkflowTrigger, redis. Event-driven trigger; 14 nodes.
Source: https://github.com/zerAda/RestaurantAgentAutomation/blob/41a4d42dcd66e57b1e87b4750c0fd5fbf7058f68/workflows/W5_OUT_WA.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
W4.1 - ROUTER (State + Voice). Uses executeWorkflowTrigger, postgres, redis. Event-driven trigger; 30 nodes.
W7 - OUT Messenger Sender (Meta Send API). Uses executeWorkflowTrigger, redis. Event-driven trigger; 14 nodes.
Splitout Redis. Uses executeWorkflowTrigger, n8n, redis, splitOut. Event-driven trigger; 46 nodes.
3770. Uses executeWorkflowTrigger, n8n, redis, agent. Event-driven trigger; 46 nodes.
Designing agent tools for outcome rather than utility has been a long recommended practice of mine and it applies well when it comes to building MCP servers; In gist, agents to be making the least amo