SaaS operations teams — RevOps, Customer Ops, Growth — spend a surprising amount of time on work that should be automated: sending trial drip emails, monitoring subscription events, tracking health scores, chasing onboarding completion, and assembling weekly briefings from five different spreadsheets.
These five n8n workflows cover the core SaaS ops automation layer. Each includes import-ready JSON.
What is n8n?
n8n is a free, open-source workflow automation platform. Self-hosted means your Stripe subscription data, customer health scores, trial pipeline, and go-to-market metrics stay on your own infrastructure — not routed through Zapier or Make's cloud. For SaaS companies under SOC 2 review, that matters: sending customer PII through third-party iPaaS cloud is a finding.
Workflow 1: Trial-to-Paid Conversion Drip Sequence
Most trial conversions happen during the first two weeks. A well-timed email sequence dramatically increases conversion rates without manual effort from your CS team.
Nodes: Webhook (trial started) → Gmail (Day 1) → Wait (3d) → Gmail (Day 4) → Wait (4d) → Gmail (Day 8) → Wait (3d) → Gmail (Day 11 offer) → Google Sheets (log)
{
"nodes": [
{
"name": "Trial Started Webhook",
"type": "n8n-nodes-base.webhook",
"parameters": { "path": "trial-started", "responseMode": "responseNode" }
},
{
"name": "Day 1 Welcome Email",
"type": "n8n-nodes-base.gmail",
"parameters": {
"to": "={{$json.body.email}}",
"subject": "Welcome to your trial — here is how to get value fast",
"message": "Hi {{$json.body.first_name}},\n\nYour 14-day trial is active. Here is your quick-start checklist:\n\n1. Connect your first data source (takes 3 minutes)\n2. Run the demo workflow\n3. Book a 20-minute setup call: {{$env.BOOKING_LINK}}\n\nReply to this email with any questions.\n\nFlowKit Team"
}
},
{
"name": "Log to Sheets",
"type": "n8n-nodes-base.googleSheets",
"parameters": { "operation": "append", "sheetId": "YOUR_TRIALS_SHEET_ID", "range": "Trials!A:F" }
},
{
"name": "Wait 3 Days",
"type": "n8n-nodes-base.wait",
"parameters": { "unit": "days", "amount": 3 }
},
{
"name": "Day 4 Use Case Email",
"type": "n8n-nodes-base.gmail",
"parameters": {
"to": "={{$node['Trial Started Webhook'].json.body.email}}",
"subject": "3 workflows your team can run today",
"message": "Hi {{$node['Trial Started Webhook'].json.body.first_name}},\n\nHere are the three automations teams in your space set up first:\n\n1. Auto-triage support tickets by priority\n2. Daily Slack digest of key metrics\n3. New lead → CRM + welcome email sequence\n\nFull JSON for each: {{$env.STORE_URL}}\n\nFlowKit Team"
}
}
]
}
What this handles:
- Day 1: onboarding quick-start (reduces time-to-first-value)
- Day 4: use-case inspiration (reduces churn from "I don't know where to start")
- Day 8: customer story / social proof (builds confidence before trial end)
- Day 11: limited offer or demo invitation (last call with urgency)
- Sheets log tracks drip_started timestamp and email address for cohort analysis
Workflow 2: Subscription Event Alert & Win-Back
When a customer cancels, your RevOps and CS teams should know instantly. And 24 hours later — after the emotion fades — send a win-back email automatically.
Nodes: Stripe Webhook (subscription.deleted) → Code (extract fields) → Slack (#revenue-ops) → Google Sheets (churn log) → Wait (24h) → Gmail (win-back offer)
{
"nodes": [
{
"name": "Stripe Subscription Deleted",
"type": "n8n-nodes-base.webhook",
"parameters": { "path": "stripe-events", "responseMode": "responseNode" }
},
{
"name": "Extract Subscription Data",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const evt = $json.body; if (evt.type !== 'customer.subscription.deleted') return []; const sub = evt.data.object; const mrr = (sub.items.data[0].price.unit_amount / 100).toFixed(2); return [{ json: { customer_id: sub.customer, plan: sub.items.data[0].price.nickname, mrr_lost: mrr, cancel_at: new Date(sub.canceled_at * 1000).toISOString(), reason: sub.cancellation_details?.reason || 'unknown' }}];"
}
},
{
"name": "Slack Revenue-Ops Alert",
"type": "n8n-nodes-base.slack",
"parameters": {
"channel": "#revenue-ops",
"text": "Cancellation: {{$json.plan}} — ${{$json.mrr_lost}}/mo lost. Reason: {{$json.reason}}. Win-back email queued for 24h."
}
},
{
"name": "Log to Churn Sheet",
"type": "n8n-nodes-base.googleSheets",
"parameters": { "operation": "append", "sheetId": "YOUR_CHURN_SHEET_ID", "range": "Churn!A:E" }
},
{
"name": "Wait 24 Hours",
"type": "n8n-nodes-base.wait",
"parameters": { "unit": "hours", "amount": 24 }
},
{
"name": "Win-Back Email",
"type": "n8n-nodes-base.gmail",
"parameters": {
"to": "={{$json.customer_email}}",
"subject": "We would love a second chance",
"message": "Hi,\n\nI noticed you cancelled yesterday. If the timing was off or something did not work for you, we would love to understand.\n\nIf you want to give us another shot, reply to this email and we will extend your trial by 30 days — no charge.\n\nEither way, thank you for trying us out.\n\nFlowKit Team"
}
}
]
}
Workflow 3: Customer Health Score Monitor
Churn rarely happens suddenly — it builds. This workflow scores every account daily using multiple signals and flags red accounts before they cancel.
Nodes: Schedule (daily 8AM) → Google Sheets (customer metrics) → Code (compute health score) → Code (segment RED/AMBER/GREEN) → IF RED → Slack (#cs-urgent) → Gmail CSM
{
"nodes": [
{
"name": "Daily 8AM",
"type": "n8n-nodes-base.scheduleTrigger",
"parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 8 * * *" }] } }
},
{
"name": "Read Customer Metrics",
"type": "n8n-nodes-base.googleSheets",
"parameters": { "operation": "getAll", "sheetId": "YOUR_HEALTH_SHEET_ID", "range": "Accounts!A:K" }
},
{
"name": "Compute Health Score",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "return $input.all().map(item => { const d = item.json; const login_score = Math.min(parseInt(d.logins_last_7d || 0) / 7, 1) * 30; const feature_score = parseFloat(d.feature_adoption_pct || 0) * 0.4; const nps_score = d.nps >= 8 ? 20 : d.nps >= 6 ? 10 : 0; const support_penalty = Math.min(parseInt(d.open_tickets || 0) * 5, 20); const health = Math.round(login_score + feature_score + nps_score - support_penalty); const segment = health >= 70 ? 'GREEN' : health >= 40 ? 'AMBER' : 'RED'; return { json: { ...d, health_score: health, segment } }; });"
}
},
{
"name": "Filter RED Accounts",
"type": "n8n-nodes-base.if",
"parameters": {
"conditions": { "string": [{ "value1": "={{$json.segment}}", "operation": "equal", "value2": "RED" }] }
}
},
{
"name": "Slack CS Urgent",
"type": "n8n-nodes-base.slack",
"parameters": {
"channel": "#cs-urgent",
"text": "Health alert: {{$json.company}} — score {{$json.health_score}}/100. CSM: {{$json.csm_name}}. Logins last 7d: {{$json.logins_last_7d}}. Open tickets: {{$json.open_tickets}}."
}
},
{
"name": "Email CSM",
"type": "n8n-nodes-base.gmail",
"parameters": {
"to": "={{$json.csm_email}}",
"subject": "Health alert: {{$json.company}} dropped to RED",
"message": "Hi {{$json.csm_name}},\n\n{{$json.company}} health score: {{$json.health_score}}/100 (RED).\n\nKey signals:\n- Logins last 7d: {{$json.logins_last_7d}}\n- Feature adoption: {{$json.feature_adoption_pct}}%\n- Open support tickets: {{$json.open_tickets}}\n- NPS: {{$json.nps}}\n\nConsider a proactive check-in today."
}
}
]
}
The composite scoring formula (login frequency + feature adoption + NPS - support penalty) is a starting point. Adjust the weights to match your product's actual churn signals.
Workflow 4: Onboarding Completion Tracker
Customers who don't complete onboarding churn at 3x the rate of those who do. This workflow monitors every new customer's onboarding progress and nudges the ones falling behind.
Nodes: Schedule (daily 9AM) → Google Sheets (new customers) → Code (filter 7-day-old accounts with <30% completion) → Gmail (nudge customer) → Slack (CSM DM) → Sheets (log nudge_sent)
{
"nodes": [
{
"name": "Daily 9AM",
"type": "n8n-nodes-base.scheduleTrigger",
"parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * *" }] } }
},
{
"name": "Read Customer Onboarding Sheet",
"type": "n8n-nodes-base.googleSheets",
"parameters": { "operation": "getAll", "sheetId": "YOUR_ONBOARDING_SHEET_ID", "range": "Customers!A:H" }
},
{
"name": "Filter At-Risk Onboarding",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const now = Date.now(); return $input.all().filter(item => { const d = item.json; const signupMs = new Date(d.signup_date).getTime(); const daysSince = (now - signupMs) / 86400000; const pct = parseFloat(d.onboarding_pct || 0); return daysSince >= 7 && daysSince <= 14 && pct < 30 && d.nudge_sent !== 'TRUE'; });"
}
},
{
"name": "Gmail Nudge to Customer",
"type": "n8n-nodes-base.gmail",
"parameters": {
"to": "={{$json.email}}",
"subject": "Quick check-in on your setup",
"message": "Hi {{$json.first_name}},\n\nI noticed you have completed about {{$json.onboarding_pct}}% of your setup. Most teams finish in under 30 minutes.\n\nWant me to jump on a quick 15-minute call to get you the rest of the way? Book here: {{$env.BOOKING_LINK}}\n\nOr reply with what you are stuck on — happy to help.\n\nFlowKit Team"
}
},
{
"name": "Slack DM to CSM",
"type": "n8n-nodes-base.slack",
"parameters": {
"channel": "={{$json.csm_slack_id}}",
"text": "Onboarding nudge sent to {{$json.company}} ({{$json.onboarding_pct}}% complete, {{$json.days_since_signup}}d old). Consider a personal outreach if no response in 48h."
}
}
]
}
Workflow 5: Weekly Go-To-Market Briefing
Every Monday morning, leadership needs a single email with all the numbers: MRR, new signups, trial conversions, NPS, pipeline value. This workflow assembles it automatically from your operational spreadsheets.
Nodes: Schedule (Monday 8AM) → Google Sheets (3x: sales, CS, marketing) → Merge → Code (build KPI object + WoW% flags) → Code (HTML email) → Gmail (leadership BCC) → Slack (#leadership one-liner)
{
"nodes": [
{
"name": "Monday 8AM",
"type": "n8n-nodes-base.scheduleTrigger",
"parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 8 * * 1" }] } }
},
{
"name": "Read Sales Sheet",
"type": "n8n-nodes-base.googleSheets",
"parameters": { "operation": "getAll", "sheetId": "YOUR_SALES_SHEET_ID", "range": "Weekly!A:E" }
},
{
"name": "Read CS Sheet",
"type": "n8n-nodes-base.googleSheets",
"parameters": { "operation": "getAll", "sheetId": "YOUR_CS_SHEET_ID", "range": "Weekly!A:E" }
},
{
"name": "Read Marketing Sheet",
"type": "n8n-nodes-base.googleSheets",
"parameters": { "operation": "getAll", "sheetId": "YOUR_MKT_SHEET_ID", "range": "Weekly!A:E" }
},
{
"name": "Build KPI Object",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const sales = $node['Read Sales Sheet'].json; const cs = $node['Read CS Sheet'].json; const mkt = $node['Read Marketing Sheet'].json; const mrr = parseFloat(sales.current_mrr || 0); const prev_mrr = parseFloat(sales.prev_mrr || 0); const mrr_wow = prev_mrr > 0 ? ((mrr - prev_mrr) / prev_mrr * 100).toFixed(1) : '0'; const trials = parseInt(mkt.new_trials || 0); const converted = parseInt(sales.trial_conversions || 0); const conversion_rate = trials > 0 ? (converted / trials * 100).toFixed(1) : '0'; const nps = parseFloat(cs.nps_score || 0); return [{ json: { mrr, mrr_wow, trials, converted, conversion_rate, nps, pipeline: sales.pipeline_value, churn_count: cs.churn_this_week, week_ending: new Date().toISOString().split('T')[0] }}];"
}
},
{
"name": "Build HTML Email",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const d = $json; const flag = (v, threshold) => parseFloat(v) < threshold ? ' ⚠️' : ''; const html = `<h2>Weekly GTM Briefing — ${d.week_ending}</h2><table border=1 cellpadding=6><tr><th>Metric</th><th>Value</th><th>WoW</th></tr><tr><td>MRR</td><td>$${d.mrr.toLocaleString()}</td><td>${d.mrr_wow}%${flag(d.mrr_wow, 0)}</td></tr><tr><td>Trial Conversions</td><td>${d.converted} / ${d.trials}</td><td>${d.conversion_rate}%${flag(d.conversion_rate, 15)}</td></tr><tr><td>NPS</td><td>${d.nps}</td><td>${flag(d.nps, 7)}</td></tr><tr><td>Churns This Week</td><td>${d.churn_count}</td><td></td></tr><tr><td>Pipeline</td><td>$${d.pipeline}</td><td></td></tr></table>`; return [{ json: { html } }];"
}
},
{
"name": "Gmail to Leadership",
"type": "n8n-nodes-base.gmail",
"parameters": {
"to": "ceo@yourcompany.com",
"bcc": "cto@yourcompany.com,vpsales@yourcompany.com,vpcs@yourcompany.com",
"subject": "Weekly GTM Briefing — {{$json.week_ending}}",
"html": "={{$json.html}}"
}
},
{
"name": "Slack Leadership Summary",
"type": "n8n-nodes-base.slack",
"parameters": {
"channel": "#leadership",
"text": "Weekly GTM: MRR ${{$json.mrr}} ({{$json.mrr_wow}}% WoW) | Trial conversion {{$json.conversion_rate}}% | NPS {{$json.nps}} | Churn {{$json.churn_count}}"
}
}
]
}
Why self-host n8n for SaaS operations?
SaaS operational data is highly sensitive:
- Stripe events contain revenue figures and cancellation reasons — routing through Zapier/Make is a SOC 2 control gap
- Health scores incorporate usage telemetry and NPS scores — customer PII under GDPR and CCPA
- Trial pipeline data reveals your go-to-market strategy — commercially sensitive competitive intelligence
- GTM briefings contain your MRR, conversion rates, and churn figures — the kind of data you don't want on a third-party platform
Self-hosted n8n keeps all of this on your own infrastructure. Workflows are git-versioned JSON — meaning they're auditable, reviewable, and rollback-capable. No per-task pricing — just the infrastructure cost (typically < $20/month on a VPS).
| Zapier | Make | n8n Self-Hosted | |
|---|---|---|---|
| Monthly cost at 50K tasks | ~$300+ | ~$99+ | Infrastructure only |
| Stripe/CRM data location | Zapier cloud | Make cloud | Your VPC |
| SOC 2 data egress finding | Yes | Yes | No |
| Custom health score logic | Limited | Limited | Full JS/Python |
| Git-versioned workflows | No | No | Yes |
Ready-to-import versions of these workflows (plus 10 more for different ops use cases) are available at FlowKit: stripeai.gumroad.com.
Questions about adapting any of these for your SaaS stack? Drop them in the comments.
Top comments (0)