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": "W7 - OUT Messenger Sender (Meta Send API)",
"active": true,
"settings": {
"executionTimeout": 120,
"saveExecutionProgress": true,
"saveManualExecutions": true
},
"nodes": [
{
"parameters": {},
"id": "f6b6bb16-96d3-4da5-829b-6d078be46f44",
"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 * 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\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\nconst outboxMsgId = payload.outboxMsgId || payload.msgId || crypto.randomUUID();\nconst outboxKey = `ralphe:outbox:msg:${outboxMsgId}`;\nconst outboxTtl = parseInt($env.OUTBOX_REDIS_TTL_SEC || '604800', 10);\n\nconst outboxEntry = {\n channel: 'messenger',\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 },\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-msg",
"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-msg",
"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 * W7 - Messenger Sender (Meta Send API)\n * P0-07: Production-ready with retries, backoff, token masking\n * \n * STRAPI GOD MODE: Config reads from _strapiConfig first, $env fallback\n *\n * Messenger Send API:\n * POST https://graph.facebook.com/{VERSION}/{PAGE_ID}/messages\n * Body: { recipient: { id: 'PSID' }, message: { text: 'MSG' } }\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.msg_send_url || $env.MSG_SEND_URL || '').toString().trim();\nconst pageId = (cfg.msg_page_id || $env.MSG_PAGE_ID || '').toString().trim();\nconst token = (cfg.msg_api_token || $env.MSG_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 API (production) or mock\nconst isMetaApi = sendUrl.includes('graph.facebook.com') || !!pageId;\nlet url = sendUrl;\n\nif (!url && pageId) {\n url = `https://graph.facebook.com/${graphVersion}/${pageId}/messages`;\n}\n\nif (!url) {\n return [{ json: { \n ...payload,\n sent: false, \n error: 'MSG_SEND_URL_NOT_SET',\n reason: 'MSG_SEND_URL or MSG_PAGE_ID must be configured',\n timestamp\n }}];\n}\n\nif (!token) {\n return [{ json: { \n ...payload,\n sent: false, \n error: 'MSG_API_TOKEN_NOT_SET',\n reason: 'MSG_API_TOKEN must be configured',\n timestamp\n }}];\n}\n\nconst recipientId = (payload.userId || '').toString().trim();\nif (!recipientId) {\n return [{ json: { \n ...payload,\n sent: false, \n error: 'RECIPIENT_MISSING',\n reason: 'userId (Page Scoped ID) is required',\n timestamp\n }}];\n}\n\n// Build request body for Meta Graph API\nlet body;\nif (isMetaApi) {\n const text = (payload.replyText || '').toString().trim();\n \n if (Array.isArray(payload.buttons) && payload.buttons.length > 0) {\n // Button template for Messenger\n body = {\n recipient: { id: recipientId },\n message: {\n attachment: {\n type: 'template',\n payload: {\n template_type: 'button',\n text: text || 'Choose an option:',\n buttons: payload.buttons.slice(0, 3).map((btn, i) => ({\n type: 'postback',\n title: (btn.title || btn.label || '').slice(0, 20),\n payload: btn.id || `btn_${i}`\n }))\n }\n }\n }\n };\n } else {\n body = {\n recipient: { id: recipientId },\n message: { text: text || '(empty message)' }\n };\n }\n} else {\n // Mock API format\n body = {\n channel: 'messenger',\n to: recipientId,\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 with exponential backoff retry\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_msg_api' : 'mock',\n messageId: responseBody?.message_id || responseBody?.recipient_id || null,\n recipient: recipientId,\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 const delay = Math.min(retryAfter * 1000, maxDelay);\n await new Promise(r => setTimeout(r, delay));\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 const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);\n await new Promise(r => setTimeout(r, delay));\n continue;\n }\n }\n \n // Client error - don't retry (except 429)\n if (statusCode >= 400) {\n const safeResponse = response.body || response;\n return [{ json: {\n ...payload,\n sent: false,\n error: 'CLIENT_ERROR',\n statusCode,\n provider: isMetaApi ? 'meta_msg_api' : 'mock',\n response: safeResponse,\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 const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);\n await new Promise(r => setTimeout(r, delay));\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_msg_api' : 'mock',\n timestamp,\n attempts: maxRetries,\n requestBody: body\n}}];\n"
},
"id": "b7cc336b-19b0-4fdb-8396-7e4aa877e1cd",
"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-msg",
"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-msg",
"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: Prepare DLQ entry for failed message\n * Token is masked for security\n */\nconst payload = $json;\nconst timestamp = new Date().toISOString();\n\nconst dlqEntry = {\n channel: 'messenger',\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 },\n requestBody: payload.requestBody,\n response: payload.response\n};\n\nreturn [{\n json: {\n ...payload,\n _dlq: {\n entry: JSON.stringify(dlqEntry)\n }\n }\n}];\n"
},
"id": "dlq-prepare-msg",
"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-msg",
"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-msg",
"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: Final result node\nconst payload = $json;\nreturn [{ json: {\n sent: payload.sent,\n messageId: payload.messageId || null,\n recipient: payload.recipient || payload.userId,\n channel: 'messenger',\n outboxMsgId: payload._outbox?.msgId,\n error: payload.error || null,\n timestamp: new Date().toISOString()\n}}];\n"
},
"id": "end-success-msg",
"name": "END - Success",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-900,
-100
]
},
{
"parameters": {
"language": "javascript",
"jsCode": "// P0-07: Final result node (DLQ path)\nconst payload = $json;\nreturn [{ json: {\n sent: false,\n messageId: null,\n recipient: payload.userId,\n channel: 'messenger',\n outboxMsgId: payload._outbox?.msgId,\n error: payload.error,\n dlqPushed: true,\n timestamp: new Date().toISOString()\n}}];\n"
},
"id": "end-dlq-msg",
"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-msg",
"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-msg",
"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: Async mode - message queued for worker\nconst payload = $json;\nreturn [{ json: {\n queued: true,\n sent: null,\n messageId: null,\n recipient: payload.userId,\n channel: 'messenger',\n outboxMsgId: payload._outbox?.msgId,\n mode: 'async',\n timestamp: new Date().toISOString()\n}}];\n"
},
"id": "end-queued-msg",
"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
W7 - OUT Messenger Sender (Meta Send API). Uses executeWorkflowTrigger, redis. Event-driven trigger; 14 nodes.
Source: https://github.com/zerAda/RestaurantAgentAutomation/blob/41a4d42dcd66e57b1e87b4750c0fd5fbf7058f68/workflows/W7_OUT_MSG.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.
W5 - OUT WhatsApp Sender (Meta Cloud 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