This workflow corresponds to n8n.io template #15298 — we link there as the canonical source.
This workflow follows the Agent → Chat Trigger 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 →
{
"id": "jt4dTN8dKOBkuqxj",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Email Marketing Manager v4",
"tags": [
{
"id": "JaNCWi5XTaMmf9J9",
"name": "AI Employee",
"createdAt": "2026-04-03T11:53:13.004Z",
"updatedAt": "2026-04-03T11:53:13.004Z"
}
],
"nodes": [
{
"id": "98adc4cb-d1c9-48d2-ba05-9d54d0d62df7",
"name": "Workflow Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1552,
512
],
"parameters": {
"width": 752,
"height": 1300,
"content": "## Email Marketing Manager\n\n**What**\n- Native n8n chat-operated AI employee for a single wellness SaaS brand\n- Handles three intents: send campaign, report campaign, reactivate cold subscribers\n\n**Why**\n- Replace repetitive email manager execution work without pretending to replace brand judgment\n- Keep v1 narrow, testable, and draft-first for internal lists. VIDEO: https://youtu.be/gEKd9vxYWEc \n\n**Assumptions**\n- Single brand only\n- Draft first\n- Reject unknown or mixed intents\n- One shared context node handles default information for every intent.\n- Generate subject + preview text + body\n- Log only task, status, time, IDs, and notes\n- Use QuickChart when a chart improves the report\n\n**Integrations** \n- MailerLite (core email platform)\n- Google sheets\n- OpenRouter (or any AI)\n\n\n**Required setup before testing**\n- Provide Openrouter credential or change it to openAI API. \n- Connect MailerLite HTTP Bearer Auth credential\n- Connect Google Sheets OAuth2 credential\n- Replace placeholder Google Sheet URL and verified sender email\n- Replace Postgres DB with others for chat memory.\n\n\n**SAMPLE INPUTS**\n\n1. Send a campaign to long term VIP users.\n Goal: Introduce an early bird vip invite to personalised dietician. \n Key message: tailored diet buddy early access.\n CTA: Link to website\n Timing: today\n\n2. Give me a report for all campaigns\n\n3. I want to re-engage all the subscribers. Tempt with a free, limited time access (for 3 days) to personalised dietician. Share 1-2 reviews from customers on success with this feature. \nReal review data-{[ \"I have to say I was skeptical at first, but I finally stopped emotional eating and have lost 3 lbs in 2 weeks!\", \"Been a long time user of Noom. Hit a ceiling and stopped using it. This new dietician helped me tweak my macros and micros for my vegetarian diet and back on track baby! Thanks guys \ud83d\ude0d\"]} \n\n\n**Enhancements** \n- Provide brand voice via DB storage & retrieval for AI mail copy.\n- Make the emails multi modal.\n- Use RAG for context management & injecting better information to AI agent. (example : previous written samples) \n- Use native chat integrations for delivering to customers so you can integrate using Slack, WhatsApp, discord etc.\n **KEY INTEGRATION NOTES MAILERLITE** Make sure the config node - Prepare Chat Context has sender_email and reply_to configured EXACTLY. Also the email id should be approved from mailerlite otherwise sending mails fail."
},
"typeVersion": 1
},
{
"id": "adff30b3-df65-4d56-836d-d24c4f665d7d",
"name": "Intake + Routing Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-112,
928
],
"parameters": {
"color": 7,
"width": 1856,
"height": 968,
"content": "## Intake + Routing\n\n- Native n8n Chat Trigger accepts plain-text operator commands only\n- OpenRouter classifies exactly one of: `send_campaign`, `report_campaign`, `reactivate_cold`, or `unknown`\n- Mixed or unknown intents are rejected\n- Config is injected first so context is visible and reusable rather than buried in prompts\n- One bounded context filler asks only for missing details and then hands off to deterministic routing\n- Intake is capped to one follow-up round, then fails cleanly if still incomplete"
},
"typeVersion": 1
},
{
"id": "a9ce6a45-1dcb-4b3a-ad64-777bbd4bec01",
"name": "Send Flow Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
1808,
976
],
"parameters": {
"color": 7,
"width": 2600,
"height": 996,
"content": "## Send Campaign Flow\n\n- Shared context filler resolves audience/segment, goal, message, CTA, and timing before branch execution\n- Resolve audience against existing MailerLite groups and segments\n- Create draft first\n- Explicit follow-up command can send immediately or schedule an existing draft\n- No auto-created segments in v1"
},
"typeVersion": 1
},
{
"id": "f5182830-b26c-4090-992c-62623462b628",
"name": "Report Flow Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
1472,
2016
],
"parameters": {
"color": 7,
"width": 2272,
"height": 536,
"content": "## Reporting Flow\n\n- Pull MailerLite campaign stats\n- Summarise open rate, CTR, unsubscribes, and bounces only\n- Use fixed v1 benchmarks: open rate > 25%, CTR > 3%\n- Generate a compact QuickChart URL when the numbers are present"
},
"typeVersion": 1
},
{
"id": "b2a533b8-e863-4cf1-8c4c-323185facdd9",
"name": "Reactivation Flow Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
1216,
2560
],
"parameters": {
"color": 7,
"width": 2856,
"height": 660,
"content": "## Reactivation + Follow-up\n\n- Uses an existing cold/inactive segment only. You need a segment called \"Re-engage\" or modify in config.\n- cold means no opens in 4 days for testing.\n- Creates one reactivation draft only\n- Scheduled follow-up is logged for later review because native n8n chat is session-based, not a proactive push channel\n- Notes column stores follow-up metadata as JSON"
},
"typeVersion": 1
},
{
"id": "e8ac6342-f73a-46ff-b372-09ce87f57055",
"name": "Chat Trigger",
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
"position": [
48,
1296
],
"parameters": {
"options": {
"responseMode": "responseNodes"
},
"agentName": "Email Marketing Manager",
"availableInChat": true,
"agentDescription": "Drafts campaigns, reports performance, and prepares reactivation sends in MailerLite."
},
"typeVersion": 1.4
},
{
"id": "251a436d-1d86-42eb-9304-6e3d6a659fb0",
"name": "Prepare Chat Context",
"type": "n8n-nodes-base.code",
"position": [
272,
1296
],
"parameters": {
"jsCode": "const update = $json;\n\nconst text = String(update.chatInput || update.message || update.text || '').trim();\nif (!text) return [];\n\nconst config = {\n brand_name: 'Noom',\n brand_profile: 'Noom is a supportive habit-building wellness SaaS brand. Tone is calm, practical, encouraging, and never shaming. Focus on sustainable routines, small daily wins, and getting back on track.',\n sender_name: 'Dorian - Noom ',\n sender_email: 'user@example.com',\n reply_to: 'user@example.com',\n default_cta: 'Open the app and take the next small step today.',\n default_audience_name: '',\n \n default_audience_kind: 'group',\n default_reactivation_segment: 'Re- Engage',\n benchmark_open_rate: 25,\n benchmark_ctr: 3,\n quickchart_enabled: true,\n cold_rule_days: 4,\n approval_mode: 'draft_first',\n};\n\nreturn [{\n json: {\n raw_update: update,\n raw_text: text,\n session_id: update.sessionId || update.session_id || '',\n user_id: update.userId || update.user_id || '',\n received_at: new Date().toISOString(),\n config,\n config_summary: [\n 'Brand: Noom',\n 'Voice: calm, practical, encouraging, non-judgmental wellness SaaS',\n 'Default CTA: Open the app and take the next small step today.',\n 'Default reactivation segment: Inactive',\n 'Benchmarks: open rate 25%, CTR 3%',\n 'Cold subscriber rule: no opens in 4 days for test mode',\n 'Approval mode: draft first',\n ].join('\\n'),\n brand_name: config.brand_name,\n brand_profile: config.brand_profile,\n sender_name: config.sender_name,\n sender_email: config.sender_email,\n reply_to: config.reply_to,\n benchmark_open_rate: config.benchmark_open_rate,\n benchmark_ctr: config.benchmark_ctr,\n quickchart_enabled: config.quickchart_enabled,\n },\n}];"
},
"typeVersion": 2
},
{
"id": "d1db446f-0093-4cc9-831e-b3251c10dd43",
"name": "Intent OpenRouter Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"position": [
512,
1392
],
"parameters": {
"model": "anthropic/claude-3.5-haiku",
"options": {}
},
"credentials": {
"openRouterApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "d57fc9a9-faec-4281-bb7f-b60d975d11c4",
"name": "Creative OpenRouter Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"position": [
2848,
1408
],
"parameters": {
"model": "anthropic/claude-haiku-4.5",
"options": {}
},
"credentials": {
"openRouterApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "ff8e176a-0eb9-42a7-b756-b5821df68806",
"name": "AI Intent Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
496,
1168
],
"parameters": {
"text": "={{ $json.toJsonString() }}",
"options": {
"systemMessage": "You resolve reusable context and route messages for an Email Marketing Manager AI employee.\nReturn short questions if responding to user otherwise final output is a raw JSON ONLY.\nUnderstand user request and assign to an intent.\n\nAllowed intents: send_campaign, report_campaign, reactivate_cold, unknown.\nReject mixed-intent requests. If more than one intent is requested, set intent to unknown and ask the user to send one task at a time.\nUnknown or unsupported requests must remain unknown.\nUse the business config provided to infer reasonable defaults when they are explicit in config.By default use audience kind of group if unclear from user input instead of going back and forth to user.\nThis workflow is single-brand, native n8n chat-only, MailerLite-only, and draft-first. \nWhen deriving any value from default config, ensure you use the exact value match without any changes.\n\nAllowed send_campaign send requests: create_draft, send_draft and schedule_draft. DO NOT create your own values. \ncreate _draft is typically the first request to start a new campaign. If not information given, use this as send request.\nFor create_draft send requests, collect a resolved context object with audience or segment, campaign goal, key message or offer, CTA. Ensure the defaults are approporiately used and minimise back and forth with user unless mandatory.\nFor follow-up send commands, keep intent as send_campaign and set send_action to send_draft or schedule_draft.\nThe workflow already has information for send_draft so you dont need to ask the user.\n\nFor report requests, report_campaign_name may be empty to mean the latest relevant sent campaign.\n\nFor reactivate_cold, use the configured default segment when appropriate.\nUse short clarification questions ONLY if required.\n\nUpon successful resolution of intent, Return JSON in this format:\n{\n \"intent\": \"unknown\",\n \"send_action\": \"none\",\n \"audience_name\": null,\n \"audience_kind\": null,\n \"campaign_name\": null,\n \"campaign_id\": null,\n \"campaign_goal\": null,\n \"key_message\": null,\n \"cta\": null,\n \"send_timing\": null,\n \"scheduled_iso\": null,\n \"report_campaign_name\": null,\n \"reactivation_segment\": null,\n \"clarification_question\": null,\n \"is_multi_intent\": false,\n \"confidence\": null,\n \"notes\": null\n}"
},
"promptType": "define"
},
"typeVersion": 3.1
},
{
"id": "0ca96592-d0d7-4308-aa3f-44a17825a93f",
"name": "Parse Intent",
"type": "n8n-nodes-base.code",
"position": [
848,
1168
],
"parameters": {
"jsCode": "function parseJson(text) {\n const cleaned = String(text || '').trim().replace(/^\\`\\`\\`json\\s*/i, '').replace(/^\\`\\`\\`/, '').replace(/\\`\\`\\`$/, '').trim();\n const first = cleaned.indexOf('{');\n const last = cleaned.lastIndexOf('}');\n const candidate = first >= 0 && last >= first ? cleaned.slice(first, last + 1) : cleaned;\n return JSON.parse(candidate || '{}');\n}\n\nconst content =\n (typeof $json.output === 'string' && $json.output) ||\n (typeof $json.text === 'string' && $json.text) ||\n (typeof $json.response === 'string' && $json.response) ||\n $json.choices?.[0]?.message?.content ||\n '{}';\nconst parsed = typeof $json.output === 'object' && $json.output !== null ? $json.output : parseJson(content);\nconst context = $('Prepare Chat Context').first().json;\nconst confidence = typeof parsed.confidence === 'number' ? parsed.confidence : null;\nconst config = context.config || {};\n\nconst resolved = {\n ...context,\n intent: parsed.intent || 'unknown',\n send_action: parsed.send_action || 'none',\n audience_name: parsed.audience_name || config.default_audience_name || '',\n audience_kind: parsed.audience_kind || config.default_audience_kind || 'unknown',\n campaign_name: parsed.campaign_name || '',\n campaign_id: parsed.campaign_id || '',\n campaign_goal: parsed.campaign_goal || '',\n key_message: parsed.key_message || '',\n cta: parsed.cta || config.default_cta || '',\n send_timing: parsed.send_timing || '',\n scheduled_iso: parsed.scheduled_iso || '',\n report_campaign_name: parsed.report_campaign_name || '',\n reactivation_segment: parsed.reactivation_segment || config.default_reactivation_segment || '',\n is_multi_intent: parsed.is_multi_intent === true,\n confidence,\n notes: parsed.notes || '',\n};\n\nconst missingFields = [];\nif (resolved.intent === 'send_campaign' && resolved.send_action === 'create_draft') {\n if (!resolved.audience_name) missingFields.push('audience or segment');\n if (!resolved.campaign_goal) missingFields.push('campaign goal');\n if (!resolved.key_message) missingFields.push('key message or offer');\n if (!resolved.cta) missingFields.push('CTA');\n if (!resolved.send_timing) missingFields.push('send timing');\n} else if (resolved.intent === 'send_campaign' && resolved.send_action === 'send_draft') {\n if (!resolved.campaign_id) missingFields.push('draft campaign ID');\n} else if (resolved.intent === 'send_campaign' && resolved.send_action === 'schedule_draft') {\n if (!resolved.campaign_id) missingFields.push('draft campaign ID');\n if (!resolved.scheduled_iso && !resolved.send_timing) missingFields.push('schedule time');\n} else if (resolved.intent === 'reactivate_cold') {\n if (!resolved.reactivation_segment) missingFields.push('reactivation segment');\n}\n\nconst needsClarification =\n resolved.intent === 'unknown' ||\n resolved.is_multi_intent === true ||\n confidence === null ||\n confidence < 0.75 ||\n missingFields.length > 0;\n\nlet clarificationQuestion = parsed.clarification_question || '';\nif (!clarificationQuestion) {\n if (resolved.intent === 'unknown' || resolved.is_multi_intent === true) {\n clarificationQuestion = 'Which one task do you want: send campaign, report campaign, or reactivate cold subscribers?';\n } else if (missingFields.length > 0) {\n clarificationQuestion = 'I need these details: ' + missingFields.join(', ') + '.';\n } else {\n clarificationQuestion = 'I need one clear email task at a time.';\n }\n}\n\nreturn [{\n json: {\n ...resolved,\n clarification_question: clarificationQuestion,\n missing_fields: missingFields,\n needs_clarification: needsClarification,\n },\n}];"
},
"typeVersion": 2
},
{
"id": "a81691fb-2df9-463b-9570-102713218b56",
"name": "Clarification Needed?",
"type": "n8n-nodes-base.if",
"position": [
1072,
1168
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "483ae685-3695-48d3-851a-f16b68d6f49e",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.needs_clarification }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.3
},
{
"id": "574ffc8f-7b0e-4a7e-a327-ad9ae60099fa",
"name": "Intent Switch",
"type": "n8n-nodes-base.switch",
"position": [
1296,
1712
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "send_campaign",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.intent }}",
"rightValue": "send_campaign"
}
]
},
"renameOutput": true
},
{
"outputKey": "report_campaign",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.intent }}",
"rightValue": "report_campaign"
}
]
},
"renameOutput": true
},
{
"outputKey": "reactivate_cold",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.intent }}",
"rightValue": "reactivate_cold"
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3.4
},
{
"id": "46f75756-bec8-432d-9686-3a6491d7cd66",
"name": "Send Action Switch",
"type": "n8n-nodes-base.switch",
"position": [
1520,
1360
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "create_draft",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.send_action }}",
"rightValue": "create_draft"
}
]
},
"renameOutput": true
},
{
"outputKey": "send_draft",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.send_action }}",
"rightValue": "send_draft"
}
]
},
"renameOutput": true
},
{
"outputKey": "schedule_draft",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.send_action }}",
"rightValue": "schedule_draft"
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3.4
},
{
"id": "6575217c-1669-4cc5-afa8-bcac54470cf0",
"name": "Build Context Prompt 1",
"type": "n8n-nodes-base.code",
"position": [
1296,
1056
],
"parameters": {
"jsCode": "const missing = $json.missing_fields || [];\nconst known = [\n 'Intent: ' + ($json.intent || 'unknown'),\n $json.send_action && $json.send_action !== 'none' ? 'Action: ' + $json.send_action : null,\n $json.audience_name ? 'Audience: ' + $json.audience_name : null,\n $json.campaign_goal ? 'Goal: ' + $json.campaign_goal : null,\n $json.key_message ? 'Key message: ' + $json.key_message : null,\n $json.cta ? 'CTA: ' + $json.cta : null,\n $json.send_timing ? 'Timing: ' + $json.send_timing : null,\n $json.campaign_id ? 'Campaign ID: ' + $json.campaign_id : null,\n $json.report_campaign_name ? 'Report campaign: ' + $json.report_campaign_name : null,\n $json.reactivation_segment ? 'Reactivation segment: ' + $json.reactivation_segment : null,\n].filter(Boolean);\n\nconst prompt =\n $json.intent === 'unknown' || $json.is_multi_intent === true\n ? [\n 'I already have the business config. I need one clear task.',\n 'Reply with exactly one of these intents and any needed details:',\n '- send campaign',\n '- report campaign',\n '- reactivate cold subscribers',\n ].join('\\n')\n : [\n 'I already have the business config. I only need the missing details.',\n 'Missing: ' + missing.join(', ') + '.',\n '',\n ...known,\n '',\n 'Reply in one message with only the missing details.',\n ].join('\\n');\n\nreturn [{\n json: {\n ...$json,\n intake_round: 1,\n intake_prompt: prompt,\n },\n}];"
},
"typeVersion": 2
},
{
"id": "393ccaa6-c447-4d31-a1f4-20d0dcee2f82",
"name": "Parse Context 1",
"type": "n8n-nodes-base.code",
"position": [
2544,
1808
],
"parameters": {
"jsCode": "function parseJson(text) {\n const cleaned = String(text || '').trim().replace(/^\\`\\`\\`json\\s*/i, '').replace(/^\\`\\`\\`/, '').replace(/\\`\\`\\`$/, '').trim();\n const first = cleaned.indexOf('{');\n const last = cleaned.lastIndexOf('}');\n const candidate = first >= 0 && last >= first ? cleaned.slice(first, last + 1) : cleaned;\n return JSON.parse(candidate || '{}');\n}\n\nconst content =\n (typeof $json.output === 'string' && $json.output) ||\n (typeof $json.text === 'string' && $json.text) ||\n (typeof $json.response === 'string' && $json.response) ||\n $json.choices?.[0]?.message?.content ||\n '{}';\nconst parsed = typeof $json.output === 'object' && $json.output !== null ? $json.output : parseJson(content);\nconst previous = $('Build Context Prompt 1').first().json;\nconst config = previous.config || {};\n\nconst resolved = {\n ...previous,\n intent: parsed.intent || previous.intent || 'unknown',\n send_action: parsed.send_action || previous.send_action || 'none',\n audience_name: parsed.audience_name || previous.audience_name || config.default_audience_name || '',\n audience_kind: parsed.audience_kind || previous.audience_kind || config.default_audience_kind || 'unknown',\n campaign_name: parsed.campaign_name || previous.campaign_name || '',\n campaign_id: parsed.campaign_id || previous.campaign_id || '',\n campaign_goal: parsed.campaign_goal || previous.campaign_goal || '',\n key_message: parsed.key_message || previous.key_message || '',\n cta: parsed.cta || previous.cta || config.default_cta || '',\n send_timing: parsed.send_timing || previous.send_timing || '',\n scheduled_iso: parsed.scheduled_iso || previous.scheduled_iso || '',\n report_campaign_name: parsed.report_campaign_name || previous.report_campaign_name || '',\n reactivation_segment: parsed.reactivation_segment || previous.reactivation_segment || config.default_reactivation_segment || '',\n notes: parsed.notes || previous.notes || '',\n};\n\nconst missingFields = [];\nif (resolved.intent === 'send_campaign' && resolved.send_action === 'create_draft') {\n if (!resolved.audience_name) missingFields.push('audience or segment');\n if (!resolved.campaign_goal) missingFields.push('campaign goal');\n if (!resolved.key_message) missingFields.push('key message or offer');\n if (!resolved.cta) missingFields.push('CTA');\n if (!resolved.send_timing) missingFields.push('send timing');\n} else if (resolved.intent === 'send_campaign' && resolved.send_action === 'send_draft') {\n if (!resolved.campaign_id) missingFields.push('draft campaign ID');\n} else if (resolved.intent === 'send_campaign' && resolved.send_action === 'schedule_draft') {\n if (!resolved.campaign_id) missingFields.push('draft campaign ID');\n if (!resolved.scheduled_iso && !resolved.send_timing) missingFields.push('schedule time');\n} else if (resolved.intent === 'reactivate_cold') {\n if (!resolved.reactivation_segment) missingFields.push('reactivation segment');\n}\n\nreturn [{\n json: {\n ...resolved,\n missing_fields: missingFields,\n },\n}];"
},
"typeVersion": 2
},
{
"id": "80638533-9602-4a40-85bb-fc4dc102fccd",
"name": "Context Failed Reply",
"type": "n8n-nodes-base.code",
"position": [
2832,
1808
],
"parameters": {
"jsCode": "return [{\n json: {\n reply_text: [\n 'Failed.',\n 'Task: context fill',\n 'Reason: the request is still incomplete after one context round.',\n 'Next step: send one message with one clear intent and the missing details.',\n ].join('\\n'),\n },\n}];"
},
"typeVersion": 2
},
{
"id": "3c7423d6-e473-413f-863b-b8f44fd2ea77",
"name": "Fetch Groups",
"type": "n8n-nodes-base.httpRequest",
"position": [
1840,
1216
],
"parameters": {
"url": "=https://connect.mailerlite.com/api/groups?limit=1000&filter[name]={{ encodeURIComponent($json.audience_name || \"\") }}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
},
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "0a3f8494-5075-4d73-bccb-37fe547cfd35",
"name": "Fetch Segments",
"type": "n8n-nodes-base.httpRequest",
"position": [
2032,
1200
],
"parameters": {
"url": "=https://connect.mailerlite.com/api/segments?limit=1000",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
},
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "6aaa751c-2892-4ceb-8ccf-0a1c7e19392c",
"name": "Resolve Send Audience",
"type": "n8n-nodes-base.code",
"position": [
2256,
1200
],
"parameters": {
"jsCode": "const context = $('Send Action Switch').first().json;\nconst desired = (context.audience_name || '').trim().toLowerCase();\nconst groups = $('Fetch Groups').first().json.data || [];\nconst segments = $('Fetch Segments').first().json.data || [];\n\nfunction findExactOrPartial(list) {\n return (\n list.find((item) => (item.name || '').trim().toLowerCase() === desired) ||\n list.find((item) => (item.name || '').trim().toLowerCase().includes(desired))\n );\n}\n\nlet match = null;\nlet kind = 'unknown';\n\nif (context.audience_kind === 'group') {\n match = findExactOrPartial(groups);\n kind = match ? 'group' : 'unknown';\n} else if (context.audience_kind === 'segment') {\n match = findExactOrPartial(segments);\n kind = match ? 'segment' : 'unknown';\n} else {\n match = findExactOrPartial(segments);\n kind = match ? 'segment' : 'unknown';\n if (!match) {\n match = findExactOrPartial(groups);\n kind = match ? 'group' : 'unknown';\n }\n}\n\nreturn [{\n json: {\n ...context,\n audience_match: match\n ? {\n id: match.id,\n name: match.name,\n kind,\n }\n : null,\n },\n}];"
},
"typeVersion": 2
},
{
"id": "4d478af7-d8f2-412d-8e63-50301c58da32",
"name": "Send Audience Found?",
"type": "n8n-nodes-base.if",
"position": [
2544,
1264
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "4a2f2e19-7684-45da-ac2e-e17e18df9a19",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.audience_match?.id || \"\" }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "e46d9961-4863-4bd8-83e9-4d14f73064a6",
"name": "Send Audience Missing Reply",
"type": "n8n-nodes-base.code",
"position": [
3120,
1424
],
"parameters": {
"jsCode": "return [{\n json: {\n reply_text: 'Failed.\\nTask: send campaign\\nReason: MailerLite audience was not found.\\nNext step: check both the exact group or segment name in Mailerlite and your input and please try again.',\n },\n}];"
},
"typeVersion": 2
},
{
"id": "82a04faf-9e66-4f0d-abb9-01a2f87bfce7",
"name": "AI Send Copy Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
2768,
1184
],
"parameters": {
"text": "={{ $json.toJsonString() }}\n\n{{ $('Send Action Switch').item.json }}",
"options": {
"systemMessage": "You write lifecycle email copy for a single-brand wellness SaaS called Noom. \nDo not include any explanatory text before or after the JSON.\nDo not include fields not requested.\n\nTask 1:\nWrite email copy in a calm, practical, warm, concise, non-judgmental voice. Use config input to use brand voice.\n\nCreate converting subject line and copy.\nThe field you must include (use exact names): \n- subject,\n- preview_text,\n- body_markdown \n\nRules for json:\n- body_markdown should be short and plain\n- subject must be <= 255 chars\n\n\n\n\n\n"
},
"promptType": "define"
},
"typeVersion": 3.1
},
{
"id": "02a0d7e8-522b-4fad-a085-69a79ebd33b9",
"name": "Create Send Draft",
"type": "n8n-nodes-base.httpRequest",
"position": [
3568,
1184
],
"parameters": {
"url": "https://connect.mailerlite.com/api/campaigns",
"method": "POST",
"options": {},
"jsonBody": "={{$json.campaign_payload.toJsonString()}}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
},
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "69890bdc-daa1-4c80-a315-c614450f7cd3",
"name": "Send Draft Response",
"type": "n8n-nodes-base.code",
"position": [
3792,
1184
],
"parameters": {
"jsCode": "const context = $('Build Send Draft Payload').first().json;\nconst campaign = $json.data || $json;\nconst campaignId = campaign.id || '';\nconst taskId = 'EMM-' + Date.now();\n\nconst notes = JSON.stringify({\n session_id: context.session_id || '',\n audience_name: context.audience_match?.name || context.audience_name,\n audience_kind: context.audience_match?.kind || context.audience_kind,\n send_timing: context.send_timing,\n requested_schedule: context.scheduled_iso || '',\n preview_text: context.preview_text,\n});\n\nreturn [{\n json: {\n reply_text: [\n 'Done.',\n 'Campaign: ' + (campaign.name || context.campaign_payload.name),\n 'Status: draft created',\n 'Audience: ' + (context.audience_match?.name || context.audience_name),\n 'Campaign ID: ' + campaignId,\n 'Next step: reply \"SEND CAMPAIGN ' + campaignId + '\" to send immediately, or \"SCHEDULE CAMPAIGN ' + campaignId + ' 2026-04-18T09:00:00Z\" to schedule.',\n ].join('\\n'),\n log_row: {\n task_id: taskId,\n campaign_id: String(campaignId),\n task_type: 'send_campaign_draft',\n status: 'done',\n timestamp: new Date().toISOString(),\n notes,\n },\n },\n}];"
},
"typeVersion": 2
},
{
"id": "eeb66840-6e7b-4397-aca3-0359467c4847",
"name": "List Draft Campaigns",
"type": "n8n-nodes-base.httpRequest",
"position": [
1792,
1536
],
"parameters": {
"url": "=https://connect.mailerlite.com/api/campaigns?filter[status]=draft&limit=100",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
},
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "2a80a3da-32a0-40b0-8682-f3ff144a775a",
"name": "Draft Campaign Found?",
"type": "n8n-nodes-base.if",
"position": [
1968,
1552
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "93e98c49-5c65-4e79-bf5e-6fb3e708ba44",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $('List Draft Campaigns').item.json.data[0].id }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "e1b23a6b-5a01-4e0c-a692-77df1765aebb",
"name": "Draft Missing Reply",
"type": "n8n-nodes-base.code",
"position": [
2256,
1808
],
"parameters": {
"jsCode": "return [{\n json: {\n reply_text: 'Failed.\\nTask: send draft\\nReason: matching draft campaign was not found.\\nNext step: use the exact campaign ID from the draft confirmation message.',\n },\n}];"
},
"typeVersion": 2
},
{
"id": "f278e5b1-6fb0-4945-8481-72ac79406e93",
"name": "Build Delivery Payload",
"type": "n8n-nodes-base.code",
"position": [
2256,
1616
],
"parameters": {
"jsCode": "const action = $json.send_action;\nlet payload = { delivery: 'instant' };\n\nif (action === 'schedule_draft') {\n const iso = $json.scheduled_iso || '';\n const date = new Date(iso);\n if (!iso || Number.isNaN(date.getTime())) {\n throw new Error('A valid ISO schedule time is required for schedule_draft');\n }\n payload = {\n delivery: 'scheduled',\n schedule: {\n date: date.toISOString().slice(0, 10),\n hours: date.toISOString().slice(11, 13),\n minutes: date.toISOString().slice(14, 16),\n },\n };\n}\n\nreturn [{\n json: {\n ...$json,\n delivery_payload: payload,\n },\n}];"
},
"typeVersion": 2
},
{
"id": "68a6e7bd-b6b0-499f-ba85-bc7367dc4340",
"name": "Schedule Or Send Draft",
"type": "n8n-nodes-base.httpRequest",
"position": [
2544,
1616
],
"parameters": {
"url": "=https://connect.mailerlite.com/api/campaigns/{{ $json.data[0].id }}/schedule",
"method": "POST",
"options": {},
"jsonBody": "={{$json.delivery_payload}}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
},
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "ce864a56-9f84-4338-9344-3a715c862bea",
"name": "Delivery Response",
"type": "n8n-nodes-base.code",
"position": [
2832,
1616
],
"parameters": {
"jsCode": "const context = $('Build Delivery Payload').first().json;\nconst campaign = $json.data || $json;\nconst sendKind = context.send_action === 'schedule_draft' ? 'scheduled' : 'sent';\nconst baseTaskType =\n String(context.draft_campaign?.name || '').toLowerCase().includes('reactivate')\n ? 'reactivate_sent'\n : 'send_campaign_delivery';\n\nconst notes = JSON.stringify({\n session_id: context.session_id || '',\n campaign_name: campaign.name || context.draft_campaign?.name || '',\n followup_due_at:\n baseTaskType === 'reactivate_sent'\n ? new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString()\n : '',\n});\n\nreturn [{\n json: {\n reply_text: [\n 'Done.',\n 'Campaign: ' + (campaign.name || context.draft_campaign?.name || ''),\n 'Status: ' + sendKind,\n 'Campaign ID: ' + (campaign.id || context.draft_campaign?.id || ''),\n ].join('\\n'),\n log_row: {\n task_id: 'EMM-' + Date.now(),\n campaign_id: String(campaign.id || context.draft_campaign?.id || ''),\n task_type: baseTaskType,\n status: baseTaskType === 'reactivate_sent' ? 'sent_pending_followup' : sendKind,\n timestamp: new Date().toISOString(),\n notes,\n },\n },\n}];"
},
"typeVersion": 2
},
{
"id": "8a15200d-2c36-4d31-9f63-32654c82a35d",
"name": "List Sent Campaigns",
"type": "n8n-nodes-base.httpRequest",
"position": [
1520,
2192
],
"parameters": {
"url": "=https://connect.mailerlite.com/api/campaigns?filter[status]=sent&limit=25",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
},
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "509c3876-3970-4933-80c4-e1ffe553ab71",
"name": "Resolve Report Campaign",
"type": "n8n-nodes-base.code",
"position": [
1744,
2192
],
"parameters": {
"jsCode": "const context = $('Parse Intent').first().json;\nconst campaigns = $json.data || [];\nconst desired = (context.report_campaign_name || '').trim().toLowerCase();\n\nlet campaign = null;\nif (desired) {\n campaign =\n campaigns.find((item) => String(item.name || '').trim().toLowerCase() === desired) ||\n campaigns.find((item) => String(item.name || '').trim().toLowerCase().includes(desired)) ||\n null;\n}\nif (!campaign && campaigns.length) {\n campaign = campaigns[0];\n}\n\nreturn [{\n json: {\n ...context,\n report_campaign: campaign,\n },\n}];"
},
"typeVersion": 2
},
{
"id": "e5071987-23b8-4d0d-a263-8efbefd15df0",
"name": "Report Campaign Found?",
"type": "n8n-nodes-base.if",
"position": [
1968,
2192
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.report_campaign?.id || \"\" }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "d8c2c978-53b8-4a5b-b3bd-116d54e269e1",
"name": "Report Missing Reply",
"type": "n8n-nodes-base.code",
"position": [
2256,
2400
],
"parameters": {
"jsCode": "return [{\n json: {\n reply_text: 'Failed.\\nTask: report campaign\\nReason: no sent campaign matched that request.\\nNext step: use the latest campaign name or leave the message general to report on the most recent send.',\n },\n}];"
},
"typeVersion": 2
},
{
"id": "fa1395a8-0f8e-4c3d-8111-1c0649992f02",
"name": "AI Report Summary Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
2192,
2096
],
"parameters": {
"text": "={{ $json.toJsonString() }}",
"options": {
"systemMessage": "You summarise email campaign performance for a small SaaS operator. Return raw JSON only. Use only the provided stats. Benchmarks are fixed in v1: open rate above 25 percent is good, CTR above 3 percent is good. Keep the summary short and direct. Write exactly two fields: headline and summary."
},
"promptType": "define"
},
"typeVersion": 3.1
},
{
"id": "a1d2d1e1-c93e-4b53-834b-9ae0b114f112",
"name": "Parse Report Summary",
"type": "n8n-nodes-base.code",
"position": [
2544,
2112
],
"parameters": {
"jsCode": "function parseJson(text) {\n const cleaned = String(text || '').trim().replace(/^\\`\\`\\`json\\s*/i, '').replace(/^\\`\\`\\`/, '').replace(/\\`\\`\\`$/, '').trim();\n const first = cleaned.indexOf('{');\n const last = cleaned.lastIndexOf('}');\n const candidate = first >= 0 && last >= first ? cleaned.slice(first, last + 1) : cleaned;\n return JSON.parse(candidate || '{}');\n}\n\nconst content =\n (typeof $json.output === 'string' && $json.output) ||\n (typeof $json.text === 'string' && $json.text) ||\n (typeof $json.response === 'string' && $json.response) ||\n $json.choices?.[0]?.message?.content ||\n '{}';\nconst parsed = typeof $json.output === 'object' && $json.output !== null ? $json.output : parseJson(content);\nconst context = $('Resolve Report Campaign').first().json;\nconst campaign = context.report_campaign || {};\nconst stats = campaign.stats || campaign.emails?.[0]?.stats || {};\nreturn [{\n json: {\n ...context,\n report_stats: stats,\n report_headline: parsed.headline || 'Report ready.',\n report_summary: parsed.summary || '',\n },\n}];"
},
"typeVersion": 2
},
{
"id": "7c168e7a-1b3e-4538-911d-ceebb1f433da",
"name": "Build Report Chart",
"type": "n8n-nodes-base.code",
"position": [
2832,
2112
],
"parameters": {
"jsCode": "const stats = $json.report_stats || {};\nconst values = [\n Number((stats.open_rate?.float || 0) * 100).toFixed(1),\n Number((stats.click_rate?.float || 0) * 100).toFixed(1),\n Number(stats.unsubscribes_count || 0),\n Number((stats.hard_bounces_count || 0) + (stats.soft_bounces_count || 0)),\n].map(Number);\n\nconst chart = {\n type: 'bar',\n data: {\n labels: ['Open Rate', 'CTR', 'Unsubscribes', 'Bounces'],\n datasets: [\n {\n label: 'Campaign performance',\n data: values,\n backgroundColor: ['#4472C4', '#5B8FF9', '#F6BD16', '#E8684A'],\n },\n ],\n },\n options: {\n plugins: {\n legend: { display: false },\n },\n scales: {\n y: {\n beginAtZero: true,\n },\n },\n },\n};\n\nreturn [{\n json: {\n ...$json,\n chart_url: 'https://quickchart.io/chart?c=' + encodeURIComponent(JSON.stringify(chart)),\n },\n}];"
},
"typeVersion": 2
},
{
"id": "e5ec86a2-2b16-4d6a-91ff-93dac0359888",
"name": "Report Reply",
"type": "n8n-nodes-base.code",
"position": [
3120,
2112
],
"parameters": {
"jsCode": "const stats = $json.report_stats || {};\nreturn [{\n json: {\n reply_text: [\n $json.report_headline,\n 'Campaign: ' + ($json.report_campaign?.name || ''),\n 'Open rate: ' + (stats.open_rate?.string || '0%'),\n 'CTR: ' + (stats.click_rate?.string || '0%'),\n 'Unsubscribes: ' + (stats.unsubscribes_count || 0),\n 'Bounces: ' + ((stats.hard_bounces_count || 0) + (stats.soft_bounces_count || 0)),\n 'Summary: ' + $json.report_summary,\n $json.chart_url ? 'Chart: ' + $json.chart_url : '',\n ]\n .filter(Boolean)\n .join('\\n'),\n log_row: {\n task_id: 'EMM-' + Date.now(),\n campaign_id: String($json.report_campaign?.id || ''),\n task_type: 'report_campaign',\n status: 'done',\n timestamp: new Date().toISOString(),\n notes: JSON.stringify({ session_id: $json.session_id || '', chart_url: $json.chart_url }),\n },\n },\n}];"
},
"typeVersion": 2
},
{
"id": "79555486-90d6-45f0-a075-1f30ae1233e8",
"name": "Fetch Reactivation Segments",
"type": "n8n-nodes-base.httpRequest",
"position": [
1520,
2784
],
"parameters": {
"url": "=https://connect.mailerlite.com/api/segments?limit=1000",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
},
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "77ceb9cc-ef39-43a8-ae4e-2e53fce44b5c",
"name": "Resolve Reactivation Segment",
"type": "n8n-nodes-base.code",
"position": [
1744,
2784
],
"parameters": {
"jsCode": "const context = $('Parse Intent').first().json;\nconst desired = (context.reactivation_segment || 'Inactive').trim().toLowerCase();\nconst segments = $json.data || [];\nconst match =\n segments.find((item) => String(item.name || '').trim().toLowerCase() === desired) ||\n segments.find((item) => String(item.name || '').trim().toLowerCase().includes(desired)) ||\n null;\n\nreturn [{\n json: {\n ...context,\n reactivation_match: match,\n },\n}];"
},
"typeVersion": 2
},
{
"id": "9284dcf1-e2fe-4e21-b95d-d9ec403ce90a",
"name": "Reactivation Segment Found?",
"type": "n8n-nodes-base.if",
"position": [
1968,
2784
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "fcfa56bd-72a4-4bf1-841f-acbfe76e236f",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.reactivation_match?.id || \"\" }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "794d3ea4-72e0-45d0-8c20-4253775642b4",
"name": "Reactivation Segment Missing Reply",
"type": "n8n-nodes-base.code",
"position": [
2256,
2992
],
"parameters": {
"jsCode": "return [{\n json: {\n reply_text: 'Failed.\\nTask: reactivate cold subscribers\\nReason: MailerLite inactive segment was not found.\\nNext step: create or rename the segment before running reactivation.',\n },\n}];"
},
"typeVersion": 2
},
{
"id": "6104f23c-d38e-4f10-8d38-336533f843dd",
"name": "AI Reactivation Copy Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
2192,
2592
],
"parameters": {
"text": "={{ $json.toJsonString() }}",
"options": {
"systemMessage": "You write one short reactivation email for a wellness SaaS brand. Voice: supportive, practical, low pressure, calm, and useful. Return raw JSON only. Write exactly three fields: subject, preview_text, body_markdown."
},
"promptType": "define"
},
"typeVersion": 3.1
},
{
"id": "90a02d51-0388-4f7f-a7ad-95b78c041efe",
"name": "Parse Reactivation Copy",
"type": "n8n-nodes-base.code",
"position": [
2544,
2704
],
"parameters": {
"jsCode": "function parseJson(text) {\n const cleaned = String(text || '').trim().replace(/^\\`\\`\\`json\\s*/i, '').replace(/^\\`\\`\\`/, '').replace(/\\`\\`\\`$/, '').trim();\n const first = cleaned.indexOf('{');\n const last = cleaned.lastIndexOf('}');\n const candidate = first >= 0 && last >= first ? cleaned.slice(first, last + 1) : cleaned;\n return JSON.parse(candidate || '{}');\n}\n\nconst content =\n (typeof $json.output === 'string' && $json.output) ||\n (typeof $json.text === 'string' && $json.text) ||\n (typeof $json.response === 'string' && $json.response) ||\n $json.choices?.[0]?.message?.content ||\n '{}';\nconst parsed = typeof $json.output === 'object' && $json.output !== null ? $json.output : parseJson(content);\nconst context = $('Resolve Reactivation Segment').first().json;\n\nfunction markdownToHtml(markdown) {\n return String(markdown || '')\n .split(/\\n{2,}/)\n .map((block) => '<p>' + block.replace(/\\n/g, '<br>') + '</p>')\n .join('');\n}\n\nreturn [{\n json: {\n ...context,\n subject: parsed.subject || '',\n preview_text: parsed.preview_text || '',\n body_markdown: parsed.body_markdown || '',\n body_html: markdownToHtml(parsed.body_markdown || ''),\n },\n}];"
},
"typeVersion": 2
},
{
"id": "90648466-d49a-41a1-af08-1ff9f5814960",
"name": "Build Reactivation Payload",
"type": "n8n-nodes-base.code",
"position": [
2832,
2704
],
"parameters": {
"jsCode": "const campaignName = 'REACTIVATE | ' + ($json.reactivation_match?.name || 'Inactive') + ' | ' + new Date().toISOString().slice(0, 10);\nreturn [{\n json: {\n ...$json,\n campaign_payload: {\n name: campaignName.slice(0, 255),\n type: 'regular',\n emails: [\n {\n subject: $json.subject,\n from_name: $json.sender_name,\n from: $json.sender_email,\n reply_to: $json.reply_to,\n content: '<div data-preview-text=\"' + $json.preview_text.replace(/\"/g, '"') + '\">' + $json.body_html + '</div>',\n },\n ],\n segments: [String($json.reactivation_match.id)],\n },\n },\n}];"
},
"typeVersion": 2
},
{
"id": "7fa90434-32a5-4ac7-b90d-e2252a243398",
"name": "Create Reactivation Draft",
"type": "n8n-nodes-base.httpRequest",
"position": [
3120,
2704
],
"parameters": {
"url": "https://connect.mailerlite.com/api/campaigns",
"method": "POST",
"options": {},
"jsonBody": "={{$json.campaign_payload}}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
},
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "4f368c7b-ea63-4048-9729-02ef9029a241",
"name": "Reactivation Draft Response",
"type": "n8n-nodes-base.code",
"position": [
3344,
2704
],
"parameters": {
"jsCode": "const context = $('Build Reactivation Payload').first().json;\nconst campaign = $json.data || $json;\nconst notes = JSON.stringify({\n session_id: context.session_id || '',\n segment_name: context.reactivation_match?.name || 'Inactive',\n cold_rule: 'no opens in 4 days',\n});\n\nreturn [{\n json: {\n reply_text: [\n 'Done.',\n 'Campaign: ' + (campaign.name || context.campaign_payload.name),\n 'Status: draft created',\n 'Audience: ' + (context.reactivation_match?.name || 'Inactive'),\n 'Campaign ID: ' + (campaign.id || ''),\n 'Next step: reply \"SEND CAMPAIGN ' + (campaign.id || '') + '\" to send the reactivation draft.',\n ].join('\\n'),\n log_row: {\n task_id: 'EMM-' + Date.now(),\n campaign_id: String(campaign.id || ''),\n task_type: 'reactivate_cold_draft',\n status: 'done',\n timestamp: new Date().toISOString(),\n notes,\n },\n },\n}];"
},
"typeVersion": 2
},
{
"id": "34159fa5-bbde-4a58-9a6b-9e01d96ff9c3",
"name": "Log Task",
"type": "n8n-nodes-base.googleSheets",
"position": [
3344,
2112
],
"parameters": {
"columns": {
"value": {
"notes": "={{ $json.log_row.notes }}",
"status": "={{ $json.log_row.status }}",
"task_id": "={{ $json.log_row.task_id }}",
"task_type": "={{ $json.log_row.task_type }}",
"timestamp": "={{ $json.log_row.timestamp }}",
"campaign_id": "={{ $json.log_row.campaign_id }}"
},
"schema": [
{
"id": "task_id",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "task_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "campaign_id",
"type": "string",
"display": true,
"required": false,
"displayName": "campaign_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "task_type",
"type": "string",
"display": true,
"required": false,
"displayName": "task_type",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "status",
"type": "string",
"display": true,
"required": false,
"displayName": "status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "timestamp",
"type": "string",
"display": true,
"required": false,
"displayName": "timestamp",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "notes",
"type": "string",
"display": true,
"required": false,
"displayName": "notes",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "=https://docs.google.com/spreadsheets/d/17gWZZ8hPNkv3W5fKgzS55CxHwdY6oH1n-0RKiP33wj4/edit?pli=1&gid=0#gid=0"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7,
"continueOnFail": true
},
{
"id": "8cd44048-bc0a-48ae-a9b2-b92a58592b6b",
"name": "Chat",
"type": "@n8n/n8n-nodes-langchain.chat",
"position": [
3568,
2112
],
"parameters": {
"message": "=DONE! \u2714\ufe0f\n {{ $('Report Reply').item.json.reply_text }}",
"options": {}
},
"typeVersion": 1.2
},
{
"id": "33170927-d1c4-40ed-825c-b2de9ac75adf",
"name": "Chat1",
"type": "@n8n/n8n-nodes-langchain.chat",
"position": [
1520,
1168
],
"parameters": {
"message": "={{ $json.clarification_question}}",
"options": {},
"operation": "sendAndWait"
},
"typeVersion": 1.2
},
{
"id": "b158695c-bc71-4557-b83e-e4a8b00f0adb",
"name": "OpenRouter Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"position": [
2272,
2816
],
"parameters": {
"options": {}
},
"credentials": {
"openRouterApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "3bf7b05c-eaff-491a-a5f5-4bb7071a2b48",
"name": "Postgres Chat Memory",
"type": "@n8n/n8n-nodes-langchain.memoryPostgresChat",
"position": [
640,
1392
],
"parameters": {},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
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.
googleSheetsOAuth2ApihttpBearerAuthhttpHeaderAuthopenRouterApipostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
What Native n8n chat-operated AI employee for a single wellness SaaS brand Handles three intents: send campaign, report campaign, reactivate cold subscribers
Source: https://n8n.io/workflows/15298/ — 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.
It acts as a powerful scraping agent that takes a simple chat query, scours both Google Search and Google Maps for relevant businesses, scrapes their websites for contact details, and compiles an enri
Master Social Scraper (Google Sheets). Uses chatTrigger, textClassifier, lmChatOpenAi, httpRequest. Chat trigger; 36 nodes.
This n8n workflow automates the entire lead generation pipeline from discovery to outreach: Location Grid Generation and Management Generates precise lat/lng grid points covering major US cities (New
This comprehensive workflow automates the complete financial document processing pipeline using AI. Upload invoices via chat, drop expense receipts into a folder, or add bank statements - the system a
Who is this workflow for? This workflow is designed for SEO analysts, content creators, marketing agencies, and developers who need to index a website and then interact with its content as if it were