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": "PhishSim \u2013 Send Campaign Emails",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "trigger-send",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-send",
"name": "Webhook \u2013 Receive Send Job",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
240,
300
]
},
{
"parameters": {
"jsCode": "// Validate shared secret\nconst secret = $input.first().json.headers?.['x-phishsim-secret'];\nconst expected = $env['PHISHSIM_SECRET'];\nif (expected && secret !== expected) {\n throw new Error('Unauthorized: bad secret');\n}\n\nconst body = $input.first().json.body;\n\nif (!body.template || !body.targets || !Array.isArray(body.targets)) {\n throw new Error('Missing template or targets array');\n}\n\nreturn body.targets.map(target => ({\n json: {\n org_id: body.org_id,\n campaign_id: body.campaign_id,\n send_run_id: body.send_run_id,\n target_id: target.id,\n to_email: target.email,\n subject: body.template.subject,\n body_html: body.template.body_html,\n body_text: body.template.body_text,\n open_url: target.tracking.open_url,\n click_url: target.tracking.click_url,\n landing_url: target.tracking.landing_url,\n token: target.tracking.token,\n }\n}));"
},
"id": "split-targets",
"name": "Split Into Individual Emails",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
300
]
},
{
"parameters": {
"jsCode": "// Process ALL items (one per target)\nreturn $input.all().map(inputItem => {\n const item = inputItem.json;\n\n let html = item.body_html || '';\n let text = item.body_text || '';\n\n // Fallback: if body_html is empty, build from body_text\n if (!html.trim()) {\n html = `<!DOCTYPE html><html><body style=\"font-family:Arial,sans-serif;font-size:14px;line-height:1.6;color:#222;max-width:600px;margin:0 auto;padding:20px\"><pre style=\"white-space:pre-wrap;font-family:inherit\">${text}</pre></body></html>`;\n }\n\n // Replace {{CLICK_URL}} placeholder (from AI-generated HTML)\n const hasClickPlaceholder = html.includes('{{CLICK_URL}}') || html.includes('%7B%7BCLICK_URL%7D%7D');\n html = html.replace(/\\{\\{CLICK_URL\\}\\}/g, item.click_url);\n\n // Replace {{LANDING_URL}} placeholder\n html = html.replace(/\\{\\{LANDING_URL\\}\\}/g, item.landing_url);\n text = text.replace(/\\{\\{LANDING_URL\\}\\}/g, item.landing_url);\n\n // If no {{CLICK_URL}} placeholder existed, inject a fallback CTA button\n const ctaButton = `<div style=\"text-align:center;margin:28px 0;padding:0 20px\"><a href=\"${item.click_url}\" style=\"display:inline-block;background:#1a73e8;color:#ffffff;padding:12px 28px;text-decoration:none;border-radius:5px;font-family:Arial,sans-serif;font-size:15px;font-weight:bold\">Take Action Now →</a></div>`;\n\n // Inject fallback CTA before </body>\n const inject = (hasClickPlaceholder ? '' : ctaButton);\n\n if (html.includes('</body>')) {\n html = html.replace('</body>', inject + '</body>');\n } else {\n html = html + inject;\n }\n\n return { json: { ...item, body_html: html, body_text: text } };\n});"
},
"id": "inject-tracking",
"name": "Inject Tracking Links",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
680,
300
]
},
{
"parameters": {
"fromEmail": "={{ $env['SMTP_FROM_EMAIL'] || 'phishsim@localhost' }}",
"toEmail": "={{ $json.to_email }}",
"subject": "={{ $json.subject }}",
"html": "={{ $json.body_html }}",
"text": "={{ $json.body_text }}",
"options": {
"allowUnauthorizedCerts": true
}
},
"id": "send-email",
"name": "Send Email via SMTP",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 1,
"position": [
900,
200
],
"continueOnFail": true,
"credentials": {
"smtp": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Report delivery for ALL targets in parallel with emailSend\n// Data comes directly from Inject Tracking (not after emailSend)\nconst appUrl = ($env['APP_URL'] || 'http://host.docker.internal:3000');\nconst secret = $env['PHISHSIM_SECRET'] || '';\n\nconst results = [];\nfor (const inputItem of $input.all()) {\n const item = inputItem.json;\n try {\n await this.helpers.request({\n method: 'POST',\n uri: `${appUrl}/api/webhooks/n8n/events`,\n headers: {\n 'Content-Type': 'application/json',\n 'x-phishsim-secret': secret\n },\n body: {\n org_id: item.org_id,\n campaign_id: item.campaign_id,\n target_id: item.target_id,\n type: 'delivered',\n metadata: { subject: item.subject }\n },\n json: true\n });\n } catch(e) {\n console.log('Delivery callback failed for', item.to_email, ':', e.message);\n }\n results.push({ json: item });\n}\nreturn results;"
},
"id": "report-delivered",
"name": "Report Delivered to App",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
400
],
"continueOnFail": true
},
{
"parameters": {
"respondWith": "json",
"responseBody": "{\"status\": \"queued\"}",
"options": {
"responseCode": 200
}
},
"id": "respond-ok",
"name": "Respond \u2013 Accepted",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
460,
480
]
}
],
"connections": {
"Webhook \u2013 Receive Send Job": {
"main": [
[
{
"node": "Split Into Individual Emails",
"type": "main",
"index": 0
},
{
"node": "Respond \u2013 Accepted",
"type": "main",
"index": 0
}
]
]
},
"Split Into Individual Emails": {
"main": [
[
{
"node": "Inject Tracking Links",
"type": "main",
"index": 0
}
]
]
},
"Inject Tracking Links": {
"main": [
[
{
"node": "Send Email via SMTP",
"type": "main",
"index": 0
},
{
"node": "Report Delivered to App",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [
"phishsim"
]
}
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.
smtp
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
PhishSim – Send Campaign Emails. Uses emailSend. Webhook trigger; 6 nodes.
Source: https://github.com/lataeq/SpecterPhish/blob/8efeef3b79cdbac0175bb27ee565d183a00559a7/n8n-workflows/02_send_campaign.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.
This workflow is designed to manage the assignment and validation of unique QR code coupons within a lead generation system with SuiteCRM.
This n8n workflow automates the generation of personalized marketing content for events, including emails, social media posts, and advertisements. Leveraging AI, it tailors content based on event deta
This workflow receives inbound leads via a webhook, normalizes and validates the payload, tags the lead’s acquisition channel using UTM/referrer/gclid signals, creates a contact in Salesmate, logs the
Automatically capture, qualify, and follow up with open house visitors in real-time
Qualification Automatique Prospects. Uses httpRequest, firestore, emailSend. Webhook trigger; 12 nodes.