# Origamy playbook spec v1. Reference: /playbooks/spec-format/
spec_version: 1
id: fitness.trial-to-member
title: Trial class to member
sector: fitness
source:
  kind: synthesized
  notes: >-
    No gym or studio has built this with us yet. Built from the base patterns: a booking form
    whose follow-up copies the slot time onto the person, an hourly
    reminder segment, a daily call-task schedule over people who finished a trial and haven't
    joined, and a staff form that records a new member. Nothing here needs an app.
derived_from: base.first-contact-follow-up
vocabulary:
  person: member
  people: members
  group: team
summary: >-
  People book a free trial through a booking form and get a confirmation that says what to bring
  and a reminder a couple of hours before. The day after the trial, a coach gets a task to call
  and ask how it went. When someone joins, staff record it on a short form and the new member gets
  a welcome. A weekly summary shows trials booked and new members.
when_to_use:
  - You run a gym, fitness studio, bootcamp or yoga or dance class and offer a free or cheap trial.
  - Trial visitors rarely hear from you afterwards, and most never come back.
  - You want a coach to make a personal call, and a simple way to track who joined.

requires:
  data_in:
    - how: booking_form
      what: "The Trial class booking form this playbook creates. Put its link in your Instagram bio, WhatsApp status and on flyers."
    - how: staff_form
      what: "The New member form this playbook creates. Front-desk staff fill it when someone joins, matched by phone."
  channels:
    - channel: sms
      for: Booking confirmation, the reminder and the welcome
      needs: An SMS provider, or Origamy managed SMS after business verification (KYC).
    - channel: email
      for: The welcome email
      needs: A verified sending domain for {{ params.email_from }}.
  events:
    - booking.created
    - booking.cancelled
    - onboarding.submitted
  traits:
    - trial_at
    - membership_status
    - membership_expires_at

parameters:
  - key: org_name
    ask: true
    question: What should messages call your gym or studio?
    label: Gym name
    type: string
    example: Northwind Fitness
  - key: gym_address
    ask: true
    question: What address or directions should the confirmation include?
    label: Address
    type: text
    example: your street, area and a landmark
  - key: coach_team
    ask: true
    question: Which team calls people after their trial? (can be one coach)
    label: Coaches
    type: team_name
    default: Coaches
  - key: trial_classes
    ask: true
    question: Which trial classes can people book? Give each a short id and the name people see.
    help: Use the same ids as your services in the service catalog.
    label: Trial classes
    type: list
    item_fields:
      key: { type: string, help: "short id, e.g. trial_class" }
      name: { type: string, help: "what people see on the form" }
    default:
      - { key: trial_class, name: Free trial class }
  - key: bring_text
    ask: true
    question: What should people bring to their trial?
    label: What to bring
    type: string
    default: water, a small towel and comfortable shoes
  - key: plans
    label: Membership plans on the New member form
    type: list
    default: [Monthly, Quarterly, Annual]
  - key: manager_team
    label: Team that gets the weekly summary
    type: team_name
    default: Managers
  - key: gym_phone
    label: Number people call to change a booking
    type: string
    default: "{{ workspace.phone }}"
  - key: reminder_hours
    label: Hours before the trial for the reminder
    type: hours
    default: 2
  - key: email_from
    label: Sender address
    type: email
    default: "{{ workspace.default_sender }}"
  - key: send_time
    label: Daily time the coach tasks are created
    type: time
    default: "09:00"
  - key: weekly_report_cron
    label: When the weekly summary arrives
    type: cron
    default: "0 8 * * 1"
  - key: greeting_fallback
    label: Greeting when a first name is missing
    type: string
    default: there
  - key: default_country
    label: Default phone country on the forms
    type: country_code
    default: "{{ workspace.country }}"
  - key: timezone
    label: Time zone
    type: timezone
    default: "{{ workspace.timezone }}"

timings:
  - A booking is confirmed by SMS straight away, with what to bring. The reminder runs every hour and goes out when the trial is less than {{ params.reminder_hours }} hours away.
  - Every day at {{ params.send_time }}, anyone whose trial ended more than 3 hours ago (up to 2 days ago) and who hasn't joined gets a call task for {{ params.coach_team }}, due in 24 hours.
  - A welcome SMS and email go out as soon as staff record a new member.
  - A weekly summary arrives to {{ params.manager_team }} (Monday at 08:00 unless you change it).

customer_must:
  - "Set up your trial class in the service catalog (length, opening hours, class times), then open the booking form's time slot question and check it is bound to that class. Booking forms can't be published until that is done."
  - Publish the Trial class booking form and the New member form. Bookmark the New member form on the front-desk computer or phone.
  - Add your coaches to the {{ params.coach_team }} team (it can be one person) and the manager to {{ params.manager_team }}.
  - "When someone joins, front-desk staff fill the New member form with the phone number the person booked with. That is what stops the coach call and starts the welcome."
  - "Switch on \"Trial booked\" and \"Trial cancelled\" first, since the other follow-ups read the trial time they save. Then connect an SMS provider (and verify your email domain), and test with your own phone."

objects:
  - ref: team_coaches
    kind: team
    about: Calls people after their trial. Can be one person.
    body: { name: "{{ params.coach_team }}" }
  - ref: team_managers
    kind: team
    about: Gets the weekly summary.
    body: { name: "{{ params.manager_team }}" }

  - ref: form_trial
    kind: form
    about: The trial booking form. A hidden field marks the person as a trial.
    body:
      name: Book a trial class
      slug: book-trial
      description: Try a class with us for free.
      draft_state:
        version: 1
        meta: { layout: classic, submit_label: Book my trial, success_message: "You're booked! We'll send you a message to confirm." }
        pages:
          - id: p1
            title: Book your trial at {{ params.org_name }}
            description: Pick a class and a time that suits you.
            fields:
              - { id: f_first, key: first_name, type: short_text, label: First name, required: true, maps_to: { mode: custom, key: first_name, trait: true } }
              - { id: f_phone, key: phone, type: phone, label: Phone number, required: true, maps_to: { mode: mapped, slot: profile.phone }, config: { country_picker: true, default_country: "{{ params.default_country }}", verify: "off" } }
              - { id: f_email, key: email, type: email, label: Email address (optional), required: false, maps_to: { mode: mapped, slot: profile.email } }
              - id: f_class
                key: trial_class
                type: single_select
                label: Which class would you like to try?
                required: true
                maps_to: { mode: mapped, slot: booking.service }
                options:
                  - $each: params.trial_classes
                    $value: { value: "{{ item.key }}", label: "{{ item.name }}" }
              - { id: f_slot, key: slot, type: date_slot, label: Pick a day and time, required: true, maps_to: { mode: mapped, slot: booking.slot } }
              - { id: f_goal, key: goal, type: short_text, label: What would you like to get out of it? (optional), required: false, maps_to: { mode: custom, key: goal } }
              - { id: f_status, key: status, type: hidden, label: "", required: false, default: trial, maps_to: { mode: custom, key: membership_status, trait: true } }
      verification_settings: { required: false }
  - ref: form_member
    kind: form
    about: Staff form to record a new member. Sets membership_status to active.
    body:
      name: New member
      slug: new-member
      description: Staff only. Fill this when someone joins, using the phone number they gave us.
      draft_state:
        version: 1
        meta: { layout: classic, submit_label: Save member, success_message: Saved. }
        pages:
          - id: p1
            title: New member
            description: One entry per new member.
            fields:
              - { id: f_phone, key: phone, type: phone, label: Member's phone, required: true, maps_to: { mode: mapped, slot: profile.phone }, config: { country_picker: true, default_country: "{{ params.default_country }}", verify: "off" } }
              - { id: f_first, key: first_name, type: short_text, label: First name, required: true, maps_to: { mode: custom, key: first_name, trait: true } }
              - { id: f_email, key: email, type: email, label: Email address (optional), required: false, maps_to: { mode: mapped, slot: profile.email } }
              - id: f_plan
                key: plan
                type: single_select
                label: Plan
                required: true
                maps_to: { mode: custom, key: membership_plan, trait: true }
                options:
                  - $each: params.plans
                    $value: { value: "{{ item }}", label: "{{ item }}" }
              - { id: f_start, key: start_date, type: date, label: Start date, required: true, maps_to: { mode: custom, key: membership_started_at, trait: true }, config: { default_today: true } }
              - { id: f_end, key: end_date, type: date, label: Membership ends, required: true, maps_to: { mode: custom, key: membership_expires_at, trait: true } }
              - { id: f_status, key: status, type: hidden, label: "", required: false, default: active, maps_to: { mode: custom, key: membership_status, trait: true } }
      verification_settings: { required: false }

  - ref: seg_trial_soon
    kind: segment
    about: Trial within the next few hours.
    body:
      name: Trial within hours
      description: trial_at within the next {{ params.reminder_hours }} hours.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: trial_at, cond_operator: within_next, value: "{{ params.reminder_hours }}h" }
  - ref: seg_trial_done
    kind: segment
    about: Trial ended more than 3 hours ago, in the last 2 days, and not yet a member.
    body:
      name: Trial done, not a member
      description: trial_at between 3 hours and 2 days ago, and membership_status is not active.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: trial_at, cond_operator: older_than, value: 3h }
          - { type: trait, trait_key: trial_at, cond_operator: within_last, value: 2d }
          - { type: trait, trait_key: membership_status, cond_operator: neq, value: active }
  - ref: seg_members
    kind: segment
    about: Everyone whose membership is active.
    body:
      name: Active members
      description: membership_status is active.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: membership_status, cond_operator: eq, value: active }

  - ref: tmpl_confirm
    kind: template
    about: Trial confirmation with what to bring.
    body:
      name: Trial booked (SMS)
      channel: sms
      category: Appointments
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "Hi {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, you're booked for a trial at {{ params.org_name }} on {{ properties.slot_start_ts | date: \"%a %-d %b, %H:%M\" }}. Please bring {{ params.bring_text }}. We're at {{ params.gym_address }}. Can't make it? Call {{ params.gym_phone }}. Reply STOP to opt out."
  - ref: tmpl_reminder
    kind: template
    about: Reminder a couple of hours before the trial.
    body:
      name: Trial today (SMS)
      channel: sms
      category: Appointments
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: see you at {{ traits.trial_at | date: \"%H:%M\" }} today for your trial. Please arrive 10 minutes early. Can't make it? Call {{ params.gym_phone }}. Reply STOP to opt out."
  - ref: tmpl_welcome_sms
    kind: template
    about: Welcome SMS for a new member.
    body:
      name: Welcome member (SMS)
      channel: sms
      category: Onboarding
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "Welcome to {{ params.org_name }}, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}! Your membership runs until {{ traits.membership_expires_at | date: \"%-d %b %Y\" }}. Questions? Call {{ params.gym_phone }}. Reply STOP to opt out."
  - ref: tmpl_welcome_email
    kind: template
    about: Welcome email for a new member.
    body:
      name: Welcome member (email)
      channel: email
      category: Onboarding
      style: html
      status: active
      subject: "Welcome to {{ params.org_name }}"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "" } }
      variables: {}
      body: >-
        <p>Hi {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>Welcome to {{ params.org_name }}. We're glad you joined. Your {{ traits.membership_plan }}
        membership runs until {{ traits.membership_expires_at | date: "%-d %b %Y" }}.</p>
        <p>We're at {{ params.gym_address }}. If you have any questions, reply to this email or
        call {{ params.gym_phone }}.</p>
        <p>{{ params.org_name }}</p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>

  - ref: wf_booked
    kind: workflow
    about: When a trial is booked, save its time on the person and confirm it.
    body:
      name: "Trial booked"
      description: Copies each trial booking's time onto the person and sends the confirmation.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: booking.created, form_id: "{{ ref.form_trial.id }}" }
      steps:
        - { id: s1, type: action, config: { action_type: set_trait, traits: { trial_at: "{{ properties.slot_start_ts }}" } } }
        - { id: s2, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_confirm.id }}" } } } }
      re_entry_policy: every_time
      re_entry_cooldown_hours: 0
      goal: {}
      exit_conditions: []
  - ref: wf_cancelled
    kind: workflow
    about: A cancelled trial clears the saved time, so no reminder or coach call follows.
    body:
      name: "Trial cancelled"
      description: Clears the saved trial time when a booking is cancelled.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: booking.cancelled, form_id: "{{ ref.form_trial.id }}" }
      steps:
        - { id: s1, type: action, config: { action_type: set_trait, traits: { trial_at: "" } } }
      re_entry_policy: every_time
      re_entry_cooldown_hours: 0
      goal: {}
      exit_conditions: []
  - ref: wf_reminder
    kind: workflow
    about: Hourly run over "trial within hours"; one reminder per trial.
    body:
      name: "Trial reminder"
      description: Every hour, a reminder to people whose trial is a few hours away.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_trial_soon.id }}", rrule: FREQ=HOURLY, starts_at: "{{ workspace.tomorrow }}T06:00", timezone: "{{ params.timezone }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_reminder.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 12
      goal: {}
      exit_conditions: []
  - ref: wf_coach_call
    kind: workflow
    about: Daily run over "trial done, not a member"; one coach call task per trial.
    body:
      name: "Trial: coach call"
      description: Every day, a call task for the coach team for people who tried a class and haven't joined. The outcome is saved.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_trial_done.id }}", rrule: FREQ=DAILY, starts_at: "{{ workspace.tomorrow }}T{{ params.send_time }}", timezone: "{{ params.timezone }}" }
      steps:
        - id: s1
          type: human_task
          config:
            human_task:
              title: "How was the trial? {{ traits.first_name | default: \"a visitor\" }}"
              instructions: |-
                {{ traits.first_name | default: "This person" }} ({{ traits.phone }}) tried a class on {{ traits.trial_at | date: "%a %-d %b" }} and hasn't joined.
                Call to ask how it went, answer questions and offer to sign them up. If they join, ask front desk to fill the New member form. Note anything they say.
              assignee_refs: [{ type: team, id: "{{ ref.team_coaches.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: joined, label: Joining, intent: positive }
                - { id: thinking, label: Still thinking, intent: neutral }
                - { id: not_interested, label: Not interested, intent: negative }
              due_hours: 24
              wait: true
              wait_timeout_hours: 168
              notify: immediate
        - { id: s2, type: action, config: { action_type: set_trait, traits: { trial_outcome: "{{ properties.task_outcome | default: \"unknown\" }}", trial_note: "{{ properties.task_note }}" } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 72
      goal: {}
      exit_conditions: []
  - ref: wf_welcome
    kind: workflow
    about: Welcome SMS and email when a new member is recorded.
    body:
      name: "New member: welcome"
      description: When staff record a new member, a welcome SMS and email.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: onboarding.submitted, form_id: "{{ ref.form_member.id }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_welcome_sms.id }}" } } } }
        - { id: s2, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_welcome_email.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 720
      goal: {}
      exit_conditions: []

  - ref: q_trials
    kind: saved_question
    about: How many trials were booked (used by the weekly summary).
    body:
      name: "Report: trial bookings"
      data_plane_id: default
      viz_type: number
      viz_settings: {}
      visibility: workspace
      query_ast:
        table: events
        filters:
          operator: and
          conditions:
            - { field: event_name, op: eq, value: booking.created }
            - { field: properties.form_id, op: eq, value: "{{ ref.form_trial.id }}" }
        aggregations: [{ fn: count, alias: bookings }]
  - ref: q_new_members
    kind: saved_question
    about: How many new members were recorded (used by the weekly summary).
    body:
      name: "Report: new members recorded"
      data_plane_id: default
      viz_type: number
      viz_settings: {}
      visibility: workspace
      query_ast:
        table: events
        filters:
          operator: and
          conditions:
            - { field: event_name, op: eq, value: onboarding.submitted }
            - { field: properties.form_id, op: eq, value: "{{ ref.form_member.id }}" }
        aggregations: [{ fn: count, alias: members }]
  - ref: report_weekly
    kind: report
    about: Trials booked, new members and active members, sent to the managers each week.
    body:
      name: "Weekly: trials and new members"
      description: Trials booked and new members recorded over the last 7 days.
      schedule_cron: "{{ params.weekly_report_cron }}"
      timezone: "{{ params.timezone }}"
      enabled: false
      spec:
        period: last_7_days
        compare: previous_period
        week_start: monday
        sections:
          - { id: w_trials, kind: saved_question, title: Trials booked, viz: number, params: { question_id: "{{ ref.q_trials.id }}" } }
          - { id: w_members, kind: saved_question, title: New members, viz: number, params: { question_id: "{{ ref.q_new_members.id }}" } }
          - id: w_active
            kind: segment_sizes
            title: Active members now
            params:
              segment_ids: ["{{ ref.seg_members.id }}"]
          - { id: w_tasks, kind: tasks_summary, title: Coach calls, params: {} }
      recipients: [{ type: team, id: "{{ ref.team_managers.id }}" }]
      channel_policy: { mode: email_only }

eval:
  name: 81_fitness_trial_to_member
  mode: task
  vertical: ""
  prompt: >-
    We run a gym and offer a free trial class. Let people book a trial through a booking form and
    text them a confirmation that tells them what to bring, then a reminder 2 hours before. The
    day after the trial, give our coaches a task to call and ask how it went, unless they've
    joined. Staff need a form to record new members, which sends a welcome text. Send me a weekly
    summary of trials and new members.
  expect_artifacts: [form, segment, workflow, message_template, report]
  forbid_terms: [discount]
