{
  "name": "automate fitness studio class attendance and member retention with AI",
  "nodes": [
    {
      "id": "3f2091b0-4120-44a0-a5a8-8b9ba6cdd086",
      "name": "When Check-in Logged",
      "type": "n8n-nodes-base.webhook",
      "position": [
        0,
        -320
      ],
      "parameters": {
        "path": "fitness/checkin",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "7796d873-0034-4471-8ad9-0a4812b3c09b",
      "name": "Normalize Check-in",
      "type": "n8n-nodes-base.set",
      "position": [
        240,
        -320
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "c1",
              "name": "checkin_id",
              "type": "string",
              "value": "={{ 'CI-' + Date.now().toString(36).toUpperCase() + Math.random().toString(36).slice(2,6).toUpperCase() }}"
            },
            {
              "id": "c2",
              "name": "member_id",
              "type": "string",
              "value": "={{ String($json.body.member_id || '').trim() }}"
            },
            {
              "id": "c3",
              "name": "class_name",
              "type": "string",
              "value": "={{ String($json.body.class_name || '').trim() }}"
            },
            {
              "id": "c4",
              "name": "checkin_date",
              "type": "string",
              "value": "={{ $json.body.checkin_date || $now.toFormat('yyyy-MM-dd') }}"
            },
            {
              "id": "c5",
              "name": "status",
              "type": "string",
              "value": "={{ String($json.body.status || 'present').toLowerCase() }}"
            },
            {
              "id": "c6",
              "name": "notes",
              "type": "string",
              "value": "={{ String($json.body.notes || '').trim() }}"
            },
            {
              "id": "c7",
              "name": "received_at",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "b185e90f-da65-4991-a844-e5cea3310a3f",
      "name": "Validate Check-in Input",
      "type": "n8n-nodes-base.if",
      "position": [
        480,
        -320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "v1",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.member_id }}",
              "rightValue": ""
            },
            {
              "id": "v2",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.class_name }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "56d52815-e95d-438e-962c-381f3930a2df",
      "name": "Read Member Roster",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        768,
        -336
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "MemberRoster"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1BkL2CkGAuYmZC2vDD6SGDqOCau7GHT-Kad6cyTLwFtE"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "7e0a0456-b46f-48c2-a33e-445cee4637b2",
      "name": "Match Member by ID",
      "type": "n8n-nodes-base.code",
      "position": [
        1008,
        -336
      ],
      "parameters": {
        "jsCode": "const norm = $('Normalize Check-in').first().json;\nconst roster = $input.all().map(i => i.json);\nconst id = String(norm.member_id || '').trim();\nlet m = null;\nfor (const r of roster) {\n  if (String(r.member_id || '').trim() === id) { m = r; break; }\n}\nif (!m) { return [{ json: { ...norm, found: false } }]; }\nreturn [{ json: { ...norm, found: true, member_name: m.member_name || '', member_email: m.member_email || '', membership: m.membership || '', join_date: m.join_date || '', email_ok: m.email_ok || 'yes' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "729faede-b9a0-4a86-819c-2fa5d6920ddf",
      "name": "Member Found in Roster",
      "type": "n8n-nodes-base.if",
      "position": [
        1248,
        -336
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f1",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.found }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "288f2531-6041-4679-9cd7-b5dc308dd591",
      "name": "Build Agent Input",
      "type": "n8n-nodes-base.set",
      "position": [
        1488,
        -352
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "b1",
              "name": "checkin_id",
              "type": "string",
              "value": "={{ $json.checkin_id }}"
            },
            {
              "id": "b2",
              "name": "member_id",
              "type": "string",
              "value": "={{ $json.member_id }}"
            },
            {
              "id": "b3",
              "name": "member_name",
              "type": "string",
              "value": "={{ $json.member_name }}"
            },
            {
              "id": "b4",
              "name": "member_email",
              "type": "string",
              "value": "={{ $json.member_email }}"
            },
            {
              "id": "b5",
              "name": "membership",
              "type": "string",
              "value": "={{ $json.membership }}"
            },
            {
              "id": "b6",
              "name": "class_name",
              "type": "string",
              "value": "={{ $json.class_name }}"
            },
            {
              "id": "b7",
              "name": "checkin_date",
              "type": "string",
              "value": "={{ $json.checkin_date }}"
            },
            {
              "id": "b8",
              "name": "status",
              "type": "string",
              "value": "={{ $json.status }}"
            },
            {
              "id": "b9",
              "name": "notes",
              "type": "string",
              "value": "={{ $json.notes }}"
            },
            {
              "id": "b10",
              "name": "join_date",
              "type": "string",
              "value": "={{ $json.join_date }}"
            },
            {
              "id": "b11",
              "name": "is_new",
              "type": "boolean",
              "value": "={{ (() => { try { const j = new Date($json.join_date); return (Date.now() - j.getTime()) / 86400000 <= 30; } catch (e) { return false; } })() }}"
            },
            {
              "id": "b12",
              "name": "received_at",
              "type": "string",
              "value": "={{ $json.received_at }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "54324788-9c89-4c82-b7e7-a2fed9ee654e",
      "name": "AI Judge Engagement and Write Message",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1696,
        -352
      ],
      "parameters": {
        "text": "={{ 'Member: ' + $json.member_name + ' | membership: ' + $json.membership + ' | class today: ' + $json.class_name + ' | check-in status: ' + $json.status + ' | notes: ' + $json.notes + ' | joined: ' + $json.join_date + ' | new member: ' + $json.is_new + ' | received at: ' + $json.received_at }}",
        "options": {
          "systemMessage": "You are the member experience assistant for a small fitness studio. A front desk class check-in just came in. Judge the member engagement: at_risk when the check-in status is no_show (write a warm, gentle nudge encouraging them to come back soon, never blaming them); new when the member joined within the last 30 days (write a short encouraging note that helps them build the habit); otherwise engaged (write a brief warm thank you). Write ONE short message under 50 words addressed to the member by first name. Never mention internal metrics or algorithms. Return ONLY valid JSON with double quoted keys and no code fences: {\\\"engagement_status\\\":\\\"at_risk|new|engaged\\\",\\\"message\\\":\\\"...\\\"}. No markdown, no emojis, no text outside the JSON."
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "092117c1-2b0f-4572-8606-dc1a6070e811",
      "name": "Engagement Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1920,
        -224
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {
          "temperature": 0.2
        },
        "builtInTools": {}
      },
      "typeVersion": 1.3
    },
    {
      "id": "59a9c027-1c4f-41c8-81a3-ba89f75ecae8",
      "name": "Parse Agent Output",
      "type": "n8n-nodes-base.code",
      "position": [
        2000,
        -352
      ],
      "parameters": {
        "jsCode": "const base = $('Build Agent Input').first().json;\nconst item = $input.first().json;\nlet raw = String(item.output || '').replace(/```json/gi, '').replace(/```/g, '').trim();\nraw = raw.replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, '\\\\');\nlet parsed = {};\ntry { parsed = JSON.parse(raw); } catch (e) { const m = raw.match(/\\{[\\s\\S]*\\}/); if (m) { try { parsed = JSON.parse(m[0]); } catch (e2) {} } }\nconst es = parsed.engagement_status || 'engaged';\nconst message = parsed.message || ('Thanks for coming to class, ' + (base.member_name || '') + '!');\nconst needsAttention = (es === 'at_risk' || String(base.status || '').toLowerCase() === 'no_show');\nreturn [{ json: { ...base, engagement_status: es, message: message, ai_ok: !!(parsed.message), needs_attention: needsAttention } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "fa1ece04-0c35-4e67-b308-1f177241dd67",
      "name": "Build Check-in Log Row",
      "type": "n8n-nodes-base.set",
      "position": [
        2208,
        -352
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "r1",
              "name": "checkin_id",
              "type": "string",
              "value": "={{ $json.checkin_id }}"
            },
            {
              "id": "r2",
              "name": "member_id",
              "type": "string",
              "value": "={{ $json.member_id }}"
            },
            {
              "id": "r3",
              "name": "member_name",
              "type": "string",
              "value": "={{ $json.member_name }}"
            },
            {
              "id": "r4",
              "name": "member_email",
              "type": "string",
              "value": "={{ $json.member_email }}"
            },
            {
              "id": "r5",
              "name": "class_name",
              "type": "string",
              "value": "={{ $json.class_name }}"
            },
            {
              "id": "r6",
              "name": "checkin_date",
              "type": "string",
              "value": "={{ $json.checkin_date }}"
            },
            {
              "id": "r7",
              "name": "status",
              "type": "string",
              "value": "={{ $json.status }}"
            },
            {
              "id": "r8",
              "name": "engagement_status",
              "type": "string",
              "value": "={{ $json.engagement_status }}"
            },
            {
              "id": "r9",
              "name": "message",
              "type": "string",
              "value": "={{ $json.message }}"
            },
            {
              "id": "r10",
              "name": "needs_attention",
              "type": "boolean",
              "value": "={{ $json.needs_attention }}"
            },
            {
              "id": "r11",
              "name": "received_at",
              "type": "string",
              "value": "={{ $json.received_at }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "8d8b9aa6-495c-4140-be96-e637cb010c36",
      "name": "Append Check-in to Log",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2448,
        -352
      ],
      "parameters": {
        "columns": {
          "value": {},
          "mappingMode": "autoMapInputData"
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "AttendanceLog"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1BkL2CkGAuYmZC2vDD6SGDqOCau7GHT-Kad6cyTLwFtE"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "9aa64ae9-03dc-42c0-af26-be0bdf50bee5",
      "name": "Build Member Message",
      "type": "n8n-nodes-base.set",
      "position": [
        2688,
        -352
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "e1",
              "name": "send_to",
              "type": "string",
              "value": "={{ $json.member_email }}"
            },
            {
              "id": "e2",
              "name": "member_name",
              "type": "string",
              "value": "={{ $json.member_name }}"
            },
            {
              "id": "e3",
              "name": "subject",
              "type": "string",
              "value": "={{ 'Thanks for coming to class - ' + $json.class_name }}"
            },
            {
              "id": "e4",
              "name": "body",
              "type": "string",
              "value": "={{ $json.message + '\\n\\nSee you at your next class!\\nSunrise Fitness Studio' }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "49648692-e524-40f8-a152-124a83067449",
      "name": "Email Member Message",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2928,
        -352
      ],
      "parameters": {
        "sendTo": "={{ $('Build Member Message').item.json.send_to }}",
        "message": "={{ $('Build Member Message').item.json.body }}",
        "options": {},
        "subject": "={{ $('Build Member Message').item.json.subject }}",
        "emailType": "text"
      },
      "typeVersion": 2.2
    },
    {
      "id": "b67b3dc1-40a2-49fd-bc68-940abfeedc54",
      "name": "If Needs Attention",
      "type": "n8n-nodes-base.if",
      "position": [
        3168,
        -352
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "a1",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $('Build Check-in Log Row').item.json.needs_attention }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "039cc9c2-0783-47e4-a926-185af51bf53f",
      "name": "Alert Manager on Discord",
      "type": "n8n-nodes-base.discord",
      "position": [
        3424,
        -432
      ],
      "parameters": {
        "content": "={{ 'Retention alert: ' + $('Build Check-in Log Row').item.json.member_name + ' | class ' + $('Build Check-in Log Row').item.json.class_name + ' | status ' + $('Build Check-in Log Row').item.json.status + ' | AI ' + $('Build Check-in Log Row').item.json.engagement_status + '. Follow up to keep them engaged.' }}",
        "guildId": {
          "__rl": true,
          "mode": "list",
          "value": "1409061244454965312",
          "cachedResultName": "Jakserver"
        },
        "options": {},
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "1409061827827994645",
          "cachedResultName": "form-contact"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "0a57e272-dcaf-4e1e-bd7b-1acbd13fc92d",
      "name": "Build Invalid Entry Alert",
      "type": "n8n-nodes-base.set",
      "position": [
        736,
        -128
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "i1",
              "name": "member_id",
              "type": "string",
              "value": "={{ $json.member_id }}"
            },
            {
              "id": "i2",
              "name": "class_name",
              "type": "string",
              "value": "={{ $json.class_name }}"
            },
            {
              "id": "i3",
              "name": "received_at",
              "type": "string",
              "value": "={{ $json.received_at }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "6f284397-87a2-45d6-98ef-2e1cd9ceb7cc",
      "name": "Alert Invalid on Discord",
      "type": "n8n-nodes-base.discord",
      "position": [
        976,
        -128
      ],
      "parameters": {
        "content": "={{ 'Invalid check-in rejected: member_id=' + $json.member_id + ' class=' + $json.class_name + ' at ' + $json.received_at + '. Missing required fields.' }}",
        "guildId": {
          "__rl": true,
          "mode": "list",
          "value": "1409061244454965312",
          "cachedResultName": "Jakserver"
        },
        "options": {},
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "1409061827827994645",
          "cachedResultName": "form-contact"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "4ef46bae-727c-4944-b02c-b420a10b44f6",
      "name": "Email Invalid Notice",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1216,
        -128
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "={{ 'A check-in was rejected because required fields were missing. Member: ' + $json.member_id + ' | Class: ' + $json.class_name + ' | At: ' + $json.received_at }}",
        "options": {},
        "subject": "Invalid class check-in record",
        "emailType": "text"
      },
      "typeVersion": 2.2
    },
    {
      "id": "f3f11734-95de-4d7c-afd8-767e9bfed994",
      "name": "Build Unknown Member Alert",
      "type": "n8n-nodes-base.set",
      "position": [
        1392,
        96
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "u1",
              "name": "member_id",
              "type": "string",
              "value": "={{ $json.member_id }}"
            },
            {
              "id": "u2",
              "name": "class_name",
              "type": "string",
              "value": "={{ $json.class_name }}"
            },
            {
              "id": "u3",
              "name": "received_at",
              "type": "string",
              "value": "={{ $json.received_at }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "e6f62192-14e1-4ff9-bb95-fb4ca329cc89",
      "name": "Alert Unknown on Discord",
      "type": "n8n-nodes-base.discord",
      "position": [
        1632,
        96
      ],
      "parameters": {
        "content": "={{ 'Unknown member check-in: id=' + $json.member_id + ' class=' + $json.class_name + ' at ' + $json.received_at + '. Member not in roster; no email sent.' }}",
        "guildId": {
          "__rl": true,
          "mode": "list",
          "value": "1409061244454965312",
          "cachedResultName": "Jakserver"
        },
        "options": {},
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "1409061827827994645",
          "cachedResultName": "form-contact"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "9dee2a24-5b62-489b-b8ad-1751a4950c0f",
      "name": "Email Unknown Notice",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1872,
        96
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "={{ 'A check-in referenced a member not in the roster. Member: ' + $json.member_id + ' | Class: ' + $json.class_name + ' | At: ' + $json.received_at }}",
        "options": {},
        "subject": "Unknown member check-in record",
        "emailType": "text"
      },
      "typeVersion": 2.2
    },
    {
      "id": "4f4fa80c-bb31-409c-a22d-eb48953597af",
      "name": "Weekly Insights Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        0,
        608
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * 1"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "d6126805-16f0-4625-9a35-ea8a104d63a1",
      "name": "Read Attendance for Report",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        240,
        608
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "AttendanceLog"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1BkL2CkGAuYmZC2vDD6SGDqOCau7GHT-Kad6cyTLwFtE"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "f2d4efd9-bc06-46b1-8df7-9b8dfdf811ad",
      "name": "Read Roster for Report",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        480,
        608
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "MemberRoster"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1BkL2CkGAuYmZC2vDD6SGDqOCau7GHT-Kad6cyTLwFtE"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "449de5eb-410b-40ad-816c-83ad97873937",
      "name": "Compute Weekly Stats",
      "type": "n8n-nodes-base.code",
      "position": [
        720,
        608
      ],
      "parameters": {
        "jsCode": "const seenIds = new Set();\nconst roster = $input.all().map(i => i.json).filter(r => { const k = String(r.member_id || '').trim(); if (!k || seenIds.has(k)) return false; seenIds.add(k); return true; });\nconst rows = $('Read Attendance for Report').all().map(i => i.json);\nconst now = new Date();\nconst diff = (now.getDay() + 6) % 7;\nconst monday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - diff, 0, 0, 0);\nconst week = rows.filter(r => { const t = Date.parse(r.checkin_date || ''); return t && t >= monday.getTime(); });\nconst unique = new Set(week.map(r => String(r.member_id || '')));\nconst byClass = {};\nweek.forEach(r => { const c = r.class_name || 'unknown'; byClass[c] = (byClass[c] || 0) + 1; });\nconst noShow = week.filter(r => String(r.status || '').toLowerCase() === 'no_show').length;\nconst noShowRate = week.length ? Math.round(noShow / week.length * 100) : 0;\nconst cutoff = new Date(now.getTime() - 14 * 86400000);\nconst atRisk = roster.filter(mm => {\n  const j = Date.parse(mm.join_date || '');\n  const older = j && (now - j) / 86400000 > 30;\n  const seenRecent = rows.some(r => { const t = Date.parse(r.checkin_date || ''); return String(r.member_id || '').trim() === String(mm.member_id || '').trim() && t && t >= cutoff.getTime(); });\n  return older && !seenRecent;\n}).map(mm => mm.member_name);\nconst newMembers = roster.filter(mm => { const j = Date.parse(mm.join_date || ''); return j && (now - j) / 86400000 <= 7; }).map(mm => mm.member_name);\nconst stats = { week_start: monday.toISOString().slice(0,10), total_checkins: week.length, unique_members: unique.size, class_breakdown: byClass, no_show_total: noShow, no_show_rate: noShowRate, at_risk_members: atRisk, new_members: newMembers };\nreturn [{ json: { ...stats, stats_blob: JSON.stringify(stats) } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "f4cab617-8fe8-42be-9dd1-65cd76abc536",
      "name": "AI Write Weekly Insight",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        960,
        608
      ],
      "parameters": {
        "text": "={{ 'Write the weekly studio report. Data: ' + $json.stats_blob }}",
        "options": {
          "systemMessage": "You are the studio analytics assistant for a small fitness studio. Given the weekly attendance stats, write a friendly digest under 120 words for the studio owner: total check-ins, unique members, the busiest class, no-show count and rate, any members at risk of churn, and new members for the week. End with one actionable recommendation. If the week was quiet or there was no data, say so plainly. Return ONLY valid JSON: {\\\"digest\\\":\\\"...\\\"}. No markdown, no emojis, no text outside the JSON."
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "e782e653-6757-4f14-83ce-6d688e83d4ed",
      "name": "Insight Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        960,
        784
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {
          "temperature": 0.2
        },
        "builtInTools": {}
      },
      "typeVersion": 1.3
    },
    {
      "id": "c101afb3-d242-4cac-9947-092be41b683e",
      "name": "Parse Weekly Insight",
      "type": "n8n-nodes-base.code",
      "position": [
        1280,
        608
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nconst stats = $('Compute Weekly Stats').first().json;\nlet raw = String(item.output || '').replace(/```json/gi, '').replace(/```/g, '').trim();\nraw = raw.replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, '\\\\');\nlet parsed = {};\ntry { parsed = JSON.parse(raw); } catch (e) { const m = raw.match(/\\{[\\s\\S]*\\}/); if (m) { try { parsed = JSON.parse(m[0]); } catch (e2) {} } }\nconst digest = parsed.digest || raw;\nreturn [{ json: { digest: digest, stats_blob: stats.stats_blob, week_start: stats.week_start, total_checkins: stats.total_checkins, unique_members: stats.unique_members, class_breakdown: JSON.stringify(stats.class_breakdown || {}), no_show_total: stats.no_show_total, no_show_rate: stats.no_show_rate, at_risk_members: JSON.stringify(stats.at_risk_members || []), new_members: JSON.stringify(stats.new_members || []) } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "729ed38b-a002-499e-ab1c-f2ad16a03c9b",
      "name": "Build Report Email",
      "type": "n8n-nodes-base.set",
      "position": [
        1520,
        608
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "m1",
              "name": "send_to",
              "type": "string",
              "value": "user@example.com"
            },
            {
              "id": "m2",
              "name": "subject",
              "type": "string",
              "value": "={{ 'Weekly studio report - week of ' + $now.toFormat('yyyy-MM-dd') }}"
            },
            {
              "id": "m3",
              "name": "body",
              "type": "string",
              "value": "={{ 'Check-ins: ' + $json.total_checkins + ' | Unique members: ' + $json.unique_members + ' | No-shows: ' + $json.no_show_total + ' (' + $json.no_show_rate + '%) | At risk: ' + $json.at_risk_members + '\\n\\n' + $json.digest }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "1e549f5f-c644-45d4-ac55-5af996aca0a3",
      "name": "Email Weekly Report",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1760,
        608
      ],
      "parameters": {
        "sendTo": "={{ $('Build Report Email').item.json.send_to }}",
        "message": "={{ $('Build Report Email').item.json.body }}",
        "options": {},
        "subject": "={{ $('Build Report Email').item.json.subject }}",
        "emailType": "text"
      },
      "typeVersion": 2.2
    },
    {
      "id": "97038b66-7c56-4359-b615-a4dacd73644f",
      "name": "Post Weekly Report on Discord",
      "type": "n8n-nodes-base.discord",
      "position": [
        2000,
        608
      ],
      "parameters": {
        "content": "={{ 'Weekly studio report: ' + $('Parse Weekly Insight').item.json.total_checkins + ' check-ins, ' + $('Parse Weekly Insight').item.json.unique_members + ' members, ' + $('Parse Weekly Insight').item.json.no_show_rate + '% no-show.\\n' + $('Parse Weekly Insight').item.json.digest }}",
        "guildId": {
          "__rl": true,
          "mode": "list",
          "value": "1409061244454965312",
          "cachedResultName": "Jakserver"
        },
        "options": {},
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "1409061827827994645",
          "cachedResultName": "form-contact"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "beab7ddd-37a7-4d60-9b23-d47cce046b0c",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -640,
        -160
      ],
      "parameters": {
        "width": 480,
        "height": 832,
        "content": "## 08-19-01 automate fitness studio class attendance and member retention with AI\n\n### How it works\n\nThe front desk records every class check-in on a webhook. The member is matched against the MemberRoster sheet and an AI agent judges whether they are at risk, new or engaged and writes a short personal message. Every check-in is logged to the AttendanceLog sheet and the member gets their message by email. No-shows and at-risk members also alert the manager on Discord. Every Monday a weekly report with attendance stats, at-risk members, new members and an AI summary is emailed to the manager and posted on Discord.\n\n### Setup steps\n\n- Create a Google Sheet with two tabs, MemberRoster and AttendanceLog, using the exact column headers from the workflow nodes.\n- Fill the MemberRoster tab with every member id, name, email, membership, join date and a yes or no email flag.\n- Connect Google Sheets, Gmail, OpenAI and Discord credentials on the matching nodes.\n- Point your front desk app at the fitness/checkin webhook.\n- Set the weekly report schedule and the manager email address.\n\n### Customization\n\nChange the AI prompts, add your own class names, or swap Discord for Telegram or Slack."
      },
      "typeVersion": 1
    },
    {
      "id": "af1f49cb-0651-4f81-8a6d-06d21233cd8c",
      "name": "Sticky Note: Check-in intake and validation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        -464
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 342,
        "content": "## Check-in intake and validation\n\nA check-in needs a member id and a class name. Missing fields are rejected and alerted on Discord plus email."
      },
      "typeVersion": 1
    },
    {
      "id": "3dbf0018-adfa-40fe-ad9b-5ca3ec4065ea",
      "name": "Sticky Note: Member lookup and AI engagement",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1408,
        -544
      ],
      "parameters": {
        "color": 7,
        "width": 1256,
        "height": 454,
        "content": "## Member lookup and AI engagement\n\nThe member is matched against the roster. The AI judges engagement from the check-in and writes the personal message that is emailed back."
      },
      "typeVersion": 1
    },
    {
      "id": "c3866d91-89d1-498e-bba2-7da8544aadb3",
      "name": "Sticky Note: Weekly retention insights",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        368
      ],
      "parameters": {
        "color": 7,
        "width": 2344,
        "height": 582,
        "content": "## Weekly retention insights\n\nEvery Monday attendance stats, at-risk members and new members are summarized by AI and emailed to the manager plus posted on Discord."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Insight Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Write Weekly Insight",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Engagement Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Judge Engagement and Write Message",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Build Agent Input": {
      "main": [
        [
          {
            "node": "AI Judge Engagement and Write Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Report Email": {
      "main": [
        [
          {
            "node": "Email Weekly Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Needs Attention": {
      "main": [
        [
          {
            "node": "Alert Manager on Discord",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match Member by ID": {
      "main": [
        [
          {
            "node": "Member Found in Roster",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Check-in": {
      "main": [
        [
          {
            "node": "Validate Check-in Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Agent Output": {
      "main": [
        [
          {
            "node": "Build Check-in Log Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Member Roster": {
      "main": [
        [
          {
            "node": "Match Member by ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Weekly Report": {
      "main": [
        [
          {
            "node": "Post Weekly Report on Discord",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Member Message": {
      "main": [
        [
          {
            "node": "Email Member Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute Weekly Stats": {
      "main": [
        [
          {
            "node": "AI Write Weekly Insight",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Member Message": {
      "main": [
        [
          {
            "node": "If Needs Attention",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Weekly Insight": {
      "main": [
        [
          {
            "node": "Build Report Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Check-in Logged": {
      "main": [
        [
          {
            "node": "Normalize Check-in",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Check-in to Log": {
      "main": [
        [
          {
            "node": "Build Member Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Check-in Log Row": {
      "main": [
        [
          {
            "node": "Append Check-in to Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Member Found in Roster": {
      "main": [
        [
          {
            "node": "Build Agent Input",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Unknown Member Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Roster for Report": {
      "main": [
        [
          {
            "node": "Compute Weekly Stats",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Write Weekly Insight": {
      "main": [
        [
          {
            "node": "Parse Weekly Insight",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Check-in Input": {
      "main": [
        [
          {
            "node": "Read Member Roster",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Invalid Entry Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Weekly Insights Trigger": {
      "main": [
        [
          {
            "node": "Read Attendance for Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert Invalid on Discord": {
      "main": [
        [
          {
            "node": "Email Invalid Notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert Unknown on Discord": {
      "main": [
        [
          {
            "node": "Email Unknown Notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Invalid Entry Alert": {
      "main": [
        [
          {
            "node": "Alert Invalid on Discord",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Unknown Member Alert": {
      "main": [
        [
          {
            "node": "Alert Unknown on Discord",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Attendance for Report": {
      "main": [
        [
          {
            "node": "Read Roster for Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Judge Engagement and Write Message": {
      "main": [
        [
          {
            "node": "Parse Agent Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Asia/Jakarta"
  },
  "active": false
}
