n8n for Sports & Recreation: 5 Automations for Clubs, Leagues, and Fitness Businesses
Running a sports club, rec center, or league means juggling registrations, schedules, facility bookings, member communications, and revenue — usually manually. Here's how to automate the repetitive parts with n8n so coaches and managers can focus on the game, not the inbox.
All workflows include import-ready JSON you can drop straight into your n8n instance.
1. Member Onboarding & Waiver Automation
The pain: New members fill a registration form and then fall through the cracks — no follow-up, no waiver confirmation, no Day 3 check-in.
The workflow:
- Trigger: Webhook (from Typeform, JotForm, or custom form)
- Code: extract name, email, membership tier, waiver_signed status
- Gmail: immediate personalized welcome email with schedule link and next steps
- Google Sheets: log new member row
- Wait 3 days → Gmail: Day 3 check-in ("how's your first week?")
- Wait 4 days → Gmail: Week 1 tips with top sessions to try
- IF member has no recorded activity after 14 days → Slack alert to membership coordinator
{
"nodes": [
{ "name": "Webhook", "type": "n8n-nodes-base.webhook", "parameters": { "httpMethod": "POST", "path": "member-signup", "responseMode": "responseNode" }, "position": [240, 300] },
{ "name": "Extract Member Data", "type": "n8n-nodes-base.code", "parameters": { "jsCode": "const d = items[0].json.body || items[0].json;\nreturn [{ json: { name: d.name || d.q1_name || '', email: d.email || d.q2_email || '', tier: d.tier || 'Standard', waiver_signed: d.waiver_signed === 'yes', joined_at: new Date().toISOString() } }];" }, "position": [460, 300] },
{ "name": "Send Welcome Email", "type": "n8n-nodes-base.gmail", "parameters": { "operation": "send", "toList": "={{ $json.email }}", "subject": "Welcome to the club, {{ $json.name }}!", "message": "Hi {{ $json.name }},\n\nYou're officially a member! Here's what to do next:\n\n1. Check your schedule at [SCHEDULE_LINK]\n2. Download the member app at [APP_LINK]\n3. Say hi at your first session — coaches are expecting you!\n\nSee you on the court/field.\n\n— The Team" }, "position": [680, 300] },
{ "name": "Log to Sheets", "type": "n8n-nodes-base.googleSheets", "parameters": { "operation": "appendOrUpdate", "sheetId": "YOUR_SHEET_ID", "range": "Members!A:F", "dataMode": "autoMap" }, "position": [680, 460] },
{ "name": "Wait 3 Days", "type": "n8n-nodes-base.wait", "parameters": { "amount": 3, "unit": "days" }, "position": [900, 300] },
{ "name": "Day 3 Check-In", "type": "n8n-nodes-base.gmail", "parameters": { "operation": "send", "toList": "={{ $json.email }}", "subject": "How's your first week going, {{ $json.name }}?", "message": "Hi {{ $json.name }},\n\nThree days in — how's it feeling? Reply to this email if you have questions about the schedule or your program.\n\n— The Team" }, "position": [1120, 300] },
{ "name": "Wait 4 More Days", "type": "n8n-nodes-base.wait", "parameters": { "amount": 4, "unit": "days" }, "position": [1340, 300] },
{ "name": "Week 1 Tips", "type": "n8n-nodes-base.gmail", "parameters": { "operation": "send", "toList": "={{ $json.email }}", "subject": "Your week 1 tips, {{ $json.name }}", "message": "Hi {{ $json.name }},\n\nOne week in — great start. Top 3 things members say make the biggest difference:\n\n1. Book sessions in advance (slots fill fast)\n2. Ask a coach for a form check after your first few sessions\n3. Join the member group chat for schedule updates\n\nSee you soon!\n— The Team" }, "position": [1560, 300] }
],
"connections": {
"Webhook": { "main": [[{ "node": "Extract Member Data", "type": "main", "index": 0 }]] },
"Extract Member Data": { "main": [[{ "node": "Send Welcome Email", "type": "main", "index": 0 }, { "node": "Log to Sheets", "type": "main", "index": 0 }]] },
"Send Welcome Email": { "main": [[{ "node": "Wait 3 Days", "type": "main", "index": 0 }]] },
"Wait 3 Days": { "main": [[{ "node": "Day 3 Check-In", "type": "main", "index": 0 }]] },
"Day 3 Check-In": { "main": [[{ "node": "Wait 4 More Days", "type": "main", "index": 0 }]] },
"Wait 4 More Days": { "main": [[{ "node": "Week 1 Tips", "type": "main", "index": 0 }]] }
}
}
2. Game/Match Schedule Notifier
The pain: Manually emailing schedule reminders to teams and parents. A reschedule = another round of messages.
The workflow:
- Trigger: Schedule (daily 7 AM)
- Google Sheets: read fixtures (date, opponent, venue, team, contact_emails, status)
- Code: filter fixtures where game is within next 48 hours AND status = "active"
- Gmail: personalized reminder with venue, time, and what to bring
- Slack → #team-notifications: one-line digest of the day's games
{
"nodes": [
{ "name": "Daily 7AM", "type": "n8n-nodes-base.scheduleTrigger", "parameters": { "rule": { "interval": [{ "field": "hours", "hoursInterval": 24 }] } }, "position": [240, 300] },
{ "name": "Get Fixtures", "type": "n8n-nodes-base.googleSheets", "parameters": { "operation": "getAll", "sheetId": "YOUR_SHEET_ID", "range": "Fixtures!A:G" }, "position": [460, 300] },
{ "name": "Filter Next 48h", "type": "n8n-nodes-base.code", "parameters": { "jsCode": "const now = new Date();\nconst cutoff = new Date(now.getTime() + 48 * 3600 * 1000);\nreturn items.filter(item => {\n const gameDate = new Date(item.json.date);\n return gameDate >= now && gameDate <= cutoff && item.json.status === 'active';\n});" }, "position": [680, 300] },
{ "name": "Send Reminder", "type": "n8n-nodes-base.gmail", "parameters": { "operation": "send", "toList": "={{ $json.contact_emails }}", "subject": "Game reminder: {{ $json.team }} vs {{ $json.opponent }}", "message": "Hi {{ $json.team }} team,\n\nGame reminder:\n\nOpponent: {{ $json.opponent }}\nDate/Time: {{ $json.date }}\nVenue: {{ $json.venue }}\n\nArrive 30 minutes early for warmup.\n— Club Admin" }, "position": [900, 300] },
{ "name": "Slack Digest", "type": "n8n-nodes-base.slack", "parameters": { "channel": "#team-notifications", "text": "Game: {{ $json.team }} vs {{ $json.opponent }} at {{ $json.venue }} ({{ $json.date }})" }, "position": [900, 460] }
],
"connections": {
"Daily 7AM": { "main": [[{ "node": "Get Fixtures", "type": "main", "index": 0 }]] },
"Get Fixtures": { "main": [[{ "node": "Filter Next 48h", "type": "main", "index": 0 }]] },
"Filter Next 48h": { "main": [[{ "node": "Send Reminder", "type": "main", "index": 0 }, { "node": "Slack Digest", "type": "main", "index": 0 }]] }
}
}
3. Facility Booking Confirmation & 24-Hour Reminder
The pain: Members book courts or pitches and then forget. No-shows waste prime slots that others wanted.
The workflow:
- Trigger: Webhook (from Mindbody, Glofox, or any booking form)
- Code: extract facility, date/time, member name/email
- Gmail: immediate confirmation with cancellation link
- Google Sheets: log booking
- Wait until 24 hours before booking → Gmail: reminder with cancellation option
{
"nodes": [
{ "name": "Booking Webhook", "type": "n8n-nodes-base.webhook", "parameters": { "httpMethod": "POST", "path": "facility-booking" }, "position": [240, 300] },
{ "name": "Extract Booking", "type": "n8n-nodes-base.code", "parameters": { "jsCode": "const d = items[0].json.body || items[0].json;\nreturn [{ json: { member_name: d.member_name || d.name, email: d.email, facility: d.facility || d.resource, booking_datetime: d.booking_datetime, booking_id: d.id || d.booking_id } }];" }, "position": [460, 300] },
{ "name": "Confirmation Email", "type": "n8n-nodes-base.gmail", "parameters": { "operation": "send", "toList": "={{ $json.email }}", "subject": "Booking confirmed: {{ $json.facility }}", "message": "Hi {{ $json.member_name }},\n\nYour booking is confirmed:\n\nFacility: {{ $json.facility }}\nDate/Time: {{ $json.booking_datetime }}\nBooking ID: {{ $json.booking_id }}\n\nNeed to cancel? Email us at least 2 hours before.\n\nSee you then!" }, "position": [680, 300] },
{ "name": "Log Booking", "type": "n8n-nodes-base.googleSheets", "parameters": { "operation": "appendOrUpdate", "sheetId": "YOUR_SHEET_ID", "range": "Bookings!A:G", "dataMode": "autoMap" }, "position": [680, 460] },
{ "name": "Wait 24h Before", "type": "n8n-nodes-base.wait", "parameters": { "amount": 1, "unit": "days" }, "position": [900, 300] },
{ "name": "24h Reminder", "type": "n8n-nodes-base.gmail", "parameters": { "operation": "send", "toList": "={{ $json.email }}", "subject": "Reminder: {{ $json.facility }} booking tomorrow", "message": "Hi {{ $json.member_name }},\n\nYour booking is tomorrow:\n\nFacility: {{ $json.facility }}\nTime: {{ $json.booking_datetime }}\n\nCan't make it? Please cancel at [CANCELLATION_LINK] so other members can book.\n\nSee you then!" }, "position": [1120, 300] }
],
"connections": {
"Booking Webhook": { "main": [[{ "node": "Extract Booking", "type": "main", "index": 0 }]] },
"Extract Booking": { "main": [[{ "node": "Confirmation Email", "type": "main", "index": 0 }, { "node": "Log Booking", "type": "main", "index": 0 }]] },
"Confirmation Email": { "main": [[{ "node": "Wait 24h Before", "type": "main", "index": 0 }]] },
"Wait 24h Before": { "main": [[{ "node": "24h Reminder", "type": "main", "index": 0 }]] }
}
}
4. Member Churn Early Warning System
The pain: Members go quiet, stop attending, and you only notice when they cancel. By then it's too late.
The workflow:
- Trigger: Monday 9 AM
- Google Sheets: read members (name, email, last_visit_date, membership_status)
- Code: calculate days since last visit. MEDIUM risk = 14–21 days, HIGH risk = 21+ days, status still "active"
- Slack → #membership-retention: at-risk member count + names
- Gmail: personalized win-back email to HIGH risk only
- Google Sheets: log outreach_sent_at to deduplicate next week
{
"nodes": [
{ "name": "Monday 9AM", "type": "n8n-nodes-base.scheduleTrigger", "parameters": { "rule": { "interval": [{ "field": "weeks", "weeksInterval": 1, "triggerAtDay": [1], "triggerAtHour": 9 }] } }, "position": [240, 300] },
{ "name": "Get Members", "type": "n8n-nodes-base.googleSheets", "parameters": { "operation": "getAll", "sheetId": "YOUR_SHEET_ID", "range": "Members!A:H" }, "position": [460, 300] },
{ "name": "Flag At-Risk", "type": "n8n-nodes-base.code", "parameters": { "jsCode": "const now = new Date();\nconst results = [];\nfor (const item of items) {\n const d = item.json;\n if (d.membership_status !== 'active') continue;\n const days = Math.floor((now - new Date(d.last_visit_date)) / 86400000);\n let risk = null;\n if (days >= 21) risk = 'HIGH';\n else if (days >= 14) risk = 'MEDIUM';\n if (risk) results.push({ json: { ...d, days_since_visit: days, risk_level: risk } });\n}\nreturn results;" }, "position": [680, 300] },
{ "name": "Slack Alert", "type": "n8n-nodes-base.slack", "parameters": { "channel": "#membership-retention", "text": "At-risk member: {{ $json.name }} ({{ $json.risk_level }} — {{ $json.days_since_visit }} days since last visit)" }, "position": [900, 460] },
{ "name": "HIGH Risk Only?", "type": "n8n-nodes-base.if", "parameters": { "conditions": { "string": [{ "value1": "={{ $json.risk_level }}", "operation": "equals", "value2": "HIGH" }] } }, "position": [900, 300] },
{ "name": "Win-Back Email", "type": "n8n-nodes-base.gmail", "parameters": { "operation": "send", "toList": "={{ $json.email }}", "subject": "We miss you, {{ $json.name }} — come back this week!", "message": "Hi {{ $json.name }},\n\nIt's been a little while since your last session — we miss having you around!\n\nYour membership is still active. Book your next session here: [BOOKING_LINK]\n\nIf something got in the way or there's anything we can do better, reply to this email.\n\n— The Team" }, "position": [1120, 300] }
],
"connections": {
"Monday 9AM": { "main": [[{ "node": "Get Members", "type": "main", "index": 0 }]] },
"Get Members": { "main": [[{ "node": "Flag At-Risk", "type": "main", "index": 0 }]] },
"Flag At-Risk": { "main": [[{ "node": "HIGH Risk Only?", "type": "main", "index": 0 }, { "node": "Slack Alert", "type": "main", "index": 0 }]] },
"HIGH Risk Only?": { "main": [[{ "node": "Win-Back Email", "type": "main", "index": 0 }], []] }
}
}
5. Weekly Sports Business Performance Report
The pain: No clear weekly picture of registrations, active members, revenue, bookings, and churn — all in one place.
The workflow:
- Trigger: Friday 5 PM
- Google Sheets: read members and revenue sheets
- Code: compute KPIs — active members, new this week, cancellations, at-risk count, weekly revenue, WoW % change
- Build HTML email with color-coded table
- Gmail → club manager
{
"nodes": [
{ "name": "Friday 5PM", "type": "n8n-nodes-base.scheduleTrigger", "parameters": { "rule": { "interval": [{ "field": "weeks", "weeksInterval": 1, "triggerAtDay": [5], "triggerAtHour": 17 }] } }, "position": [240, 300] },
{ "name": "Get Members", "type": "n8n-nodes-base.googleSheets", "parameters": { "operation": "getAll", "sheetId": "YOUR_SHEET_ID", "range": "Members!A:H" }, "position": [460, 200] },
{ "name": "Get Revenue", "type": "n8n-nodes-base.googleSheets", "parameters": { "operation": "getAll", "sheetId": "YOUR_SHEET_ID", "range": "Revenue!A:D" }, "position": [460, 400] },
{ "name": "Build Report", "type": "n8n-nodes-base.code", "parameters": { "jsCode": "const now = new Date();\nconst weekAgo = new Date(now.getTime() - 7 * 24 * 3600 * 1000);\nconst twoWeeksAgo = new Date(now.getTime() - 14 * 24 * 3600 * 1000);\nconst members = $('Get Members').all();\nconst revenues = $('Get Revenue').all();\nconst active = members.filter(i => i.json.membership_status === 'active').length;\nconst newThisWeek = members.filter(i => new Date(i.json.joined_at) >= weekAgo).length;\nconst cancelled = members.filter(i => i.json.membership_status === 'cancelled' && new Date(i.json.cancelled_at) >= weekAgo).length;\nconst atRisk = members.filter(i => i.json.membership_status === 'active' && Math.floor((now - new Date(i.json.last_visit_date)) / 86400000) >= 14).length;\nconst weekRev = revenues.filter(i => new Date(i.json.date) >= weekAgo).reduce((s, i) => s + Number(i.json.amount || 0), 0);\nconst prevRev = revenues.filter(i => { const d = new Date(i.json.date); return d >= twoWeeksAgo && d < weekAgo; }).reduce((s, i) => s + Number(i.json.amount || 0), 0);\nconst chg = prevRev > 0 ? ((weekRev - prevRev) / prevRev * 100).toFixed(1) : 'N/A';\nconst html = '<h2>Weekly Club Report — ' + now.toDateString() + '</h2><table border=1 cellpadding=6><tr><th>Metric</th><th>Value</th></tr><tr><td>Active Members</td><td>' + active + '</td></tr><tr><td>New This Week</td><td>' + newThisWeek + '</td></tr><tr><td>Cancellations</td><td>' + cancelled + '</td></tr><tr><td>At-Risk (14+ days inactive)</td><td style=color:' + (atRisk > 5 ? 'red' : 'orange') + '>' + atRisk + '</td></tr><tr><td>Weekly Revenue</td><td>$' + weekRev.toFixed(2) + ' (' + chg + '%)</td></tr></table>';\nreturn [{ json: { html, active, new_members: newThisWeek, cancelled, at_risk: atRisk, revenue: weekRev.toFixed(2) } }];" }, "position": [680, 300] },
{ "name": "Email Report", "type": "n8n-nodes-base.gmail", "parameters": { "operation": "send", "toList": "manager@yourclub.com", "subject": "Weekly Club Report: {{ $json.active }} active, ${{ $json.revenue }} revenue", "message": "={{ $json.html }}", "isHtml": true }, "position": [900, 300] }
],
"connections": {
"Friday 5PM": { "main": [[{ "node": "Get Members", "type": "main", "index": 0 }, { "node": "Get Revenue", "type": "main", "index": 0 }]] },
"Get Members": { "main": [[{ "node": "Build Report", "type": "main", "index": 0 }]] },
"Get Revenue": { "main": [[{ "node": "Build Report", "type": "main", "index": 0 }]] },
"Build Report": { "main": [[{ "node": "Email Report", "type": "main", "index": 0 }]] }
}
}
Why n8n for sports & recreation?
| Feature | n8n | Zapier | Make.com |
|---|---|---|---|
| Self-hosted (member data stays yours) | ✅ Free | ❌ Cloud only | ❌ Cloud only |
| Cost at 50k ops/month | $0 | ~$100/mo | ~$50/mo |
| Complex branching + wait nodes | ✅ Full | Limited | Limited |
| Git-versioned workflows | ✅ | ❌ | ❌ |
| Runs on a local server or Raspberry Pi | ✅ | ❌ | ❌ |
Member health data, payment history, waivers, and minors' information should stay on your own server — not on a third-party cloud platform. n8n is free, self-hosted, and handles exactly the kind of sequential, time-delayed, branching workflows that sports operations run on.
Get pre-built templates
All 5 workflows — packaged with Google Sheets templates and full node configs — are available at stripeai.gumroad.com.
Individual templates: $12–$29. Full bundle (15 workflows): $97.
Running n8n for a sports club or fitness business? Drop your use case in the comments.













