DEV Community

Alex Kane
Alex Kane

Posted on

n8n for Sports & Recreation: 5 Automations for Clubs, Leagues, and Fitness Businesses (Free Workflow JSON)

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 }]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }], []] }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)