# Origamy playbook spec v1. Reference: /playbooks/spec-format/
spec_version: 1
id: events.registration-and-reminders
title: Event registration and reminders
sector: events
source:
  kind: synthesized
  notes: >-
    No organiser has built this with us yet. Written from the base patterns: a registration form
    that tags each person with the event, staff check-in by phone at the door (the staff-form
    pattern), and one-off scheduled sends over a segment. One-off sends use an rrule with
    COUNT=1 and start times the customer gives, because a follow-up can't wait until a date.
vocabulary:
  person: guest
  people: guests
  group: organisers
summary: >-
  Registers guests with a form, confirms each by SMS and email, reminds them a week and a day
  before, and checks them in at the door by phone number. The day after, guests who came get a
  thank-you and a feedback form, and guests who didn't get a kind message. Organisers get a
  weekly count.
when_to_use:
  - You run a conference, workshop, church or school event, product launch or community day with registration.
  - People register and then forget, and you want fewer empty seats.
  - You want to thank those who came, reach those who didn't and know how many turned up.

requires:
  data_in:
    - how: public_form
      what: "The Registration form this playbook creates. Share the link on social media and WhatsApp, or print it as a QR code. The Event feedback form is another public form."
    - how: staff_form
      what: "A Door check-in form that your door team fills in (phone number only) as each guest arrives."
  channels:
    - channel: sms
      for: Confirmation, both reminders, the thank-you and the "sorry we missed you"
      needs: An SMS provider, or Origamy managed SMS after business verification (KYC).
    - channel: email
      for: The confirmation, the thank-you, the weekly report
      needs: A verified sending domain for {{ params.email_from }}.
  events:
    - onboarding.submitted
  traits:
    - registered_for
    - last_attended_event

parameters:
  - key: event_title
    ask: true
    question: What is your event called?
    label: Event name
    type: string
    example: Northwind Open Day
  - key: event_when
    ask: true
    question: When is it? (as messages should say it)
    label: Event date and time
    type: string
    example: Saturday 12 December, 10:00
  - key: event_place
    ask: true
    question: Where is it?
    label: Place
    type: text
    example: the main hall at 12 Example Road, or the online link
  - key: ticket_types
    ask: true
    question: What ticket or guest types are there?
    help: One per line. Leave one if everyone is the same.
    label: Ticket types
    type: list
    default: [General, Student, Speaker or guest]
  - key: reminder_7d_at
    ask: true
    question: When should the one-week reminder go out?
    help: The event date minus 7 days, with a time, like 2026-12-05T09:00 in your time zone.
    label: One-week reminder (date and time)
    type: string
    example: 2026-12-05T09:00
  - key: reminder_1d_at
    ask: true
    question: When should the day-before reminder go out?
    help: The event date minus 1 day, with a time, like 2026-12-11T09:00 in your time zone.
    label: Day-before reminder (date and time)
    type: string
    example: 2026-12-11T09:00
  - key: followup_at
    ask: true
    question: When should the thank-you and "sorry we missed you" messages go out?
    help: The morning after the event, like 2026-12-13T09:00 in your time zone.
    label: Day-after messages (date and time)
    type: string
    example: 2026-12-13T09:00
  - key: org_name
    label: Organiser name in messages
    type: string
    default: "{{ workspace.name }}"
  - key: event_key
    label: Short key that tells this event from others
    type: string
    default: "event-{{ workspace.today }}"
  - key: organiser_team
    label: Team that runs the event and gets the weekly report
    type: team_name
    default: Organisers
  - key: catchup_url
    label: Recording or next-event link for guests who missed it
    type: url
    default: ""
  - key: contact_phone
    label: Number guests can call
    type: string
    default: "{{ workspace.phone }}"
  - key: email_from
    label: Sender address
    type: email
    default: "{{ workspace.default_sender }}"
  - key: weekly_report_cron
    label: Weekly report (five-field cron, in the time zone below)
    type: cron
    default: "0 9 * * 1"
  - key: timezone
    label: Time zone
    type: timezone
    default: "{{ workspace.timezone }}"
  - key: default_country
    label: Default phone country on the forms
    type: country_code
    default: "{{ workspace.country }}"
  - key: greeting_fallback
    label: Greeting when a first name is missing
    type: string
    default: there

timings:
  - Confirmation by SMS and email straight after registration.
  - "One-week reminder at {{ params.reminder_7d_at }} and day-before reminder at {{ params.reminder_1d_at }} ({{ params.timezone }}), by SMS, to everyone registered for the event."
  - "At {{ params.followup_at }}, guests who were checked in get a thank-you and the feedback link, and guests who weren't get a kind SMS."
  - Weekly count for the organisers on the schedule {{ params.weekly_report_cron }}. Switch it off after the event.

customer_must:
  - "Work out the three send times from your event date and give them in your time zone: one week before, the day before, and the morning after. They are one-off settings, so run the playbook again for the next event."
  - Add your door and organising team to the {{ params.organiser_team }} team.
  - "At the door, have one person per entrance fill the Door check-in form with each guest's phone number, on a phone or tablet. Guests who aren't checked in are treated as not attending."
  - Publish the Registration, Door check-in and Event feedback forms, verify the sending domain, connect an SMS provider or finish business verification, then switch the follow-ups on. Test the whole flow with your own phone.
  - "If tickets are paid, take payment however you do now (bank transfer or a payment link) and only share the form with paid guests. This playbook doesn't collect money."

objects:
  - ref: team_organisers
    kind: team
    about: Runs the event and gets the weekly count.
    body: { name: "{{ params.organiser_team }}" }

  - ref: form_register
    kind: form
    about: The registration form. A hidden field tags each guest with the event.
    body:
      name: Event registration
      slug: event-registration
      description: "Register for {{ params.event_title }}."
      draft_state:
        version: 1
        meta: { layout: classic, submit_label: Register, success_message: "You're registered for {{ params.event_title }}. Check your phone for a confirmation." }
        pages:
          - id: p1
            title: "Register for {{ params.event_title }}"
            description: "{{ params.event_when }}"
            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_last, key: last_name, type: short_text, label: Last name, required: true, maps_to: { mode: custom, key: last_name, trait: true } }
              - { id: f_phone, key: phone, type: phone, label: Phone number (WhatsApp if you have it), 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, required: false, maps_to: { mode: mapped, slot: profile.email } }
              - id: f_ticket
                key: ticket_type
                type: single_select
                label: Which best describes you?
                required: true
                maps_to: { mode: custom, key: ticket_type }
                options:
                  - $each: params.ticket_types
                    $value: { value: "{{ item }}", label: "{{ item }}" }
              - { id: f_needs, key: special_needs, type: long_text, label: "Dietary or access needs (optional)", required: false, maps_to: { mode: custom, key: special_needs } }
              - { id: f_event, key: event, type: hidden, label: "", required: false, default: "{{ params.event_key }}", maps_to: { mode: custom, key: registered_for, trait: true } }
      verification_settings: { required: false }
  - ref: form_checkin
    kind: form
    about: Door form. Guests are checked in by phone number, and a hidden field records that they attended this event.
    body:
      name: Door check-in
      slug: door-check-in
      description: Door team only. One entry as each guest arrives.
      draft_state:
        version: 1
        meta: { layout: classic, submit_label: Check in, success_message: Checked in. Welcome. }
        pages:
          - id: p1
            title: Check in
            description: Type the phone number the guest registered with.
            fields:
              - { id: f_phone, key: phone, type: phone, label: Guest's phone, required: true, maps_to: { mode: mapped, slot: profile.phone }, config: { country_picker: true, default_country: "{{ params.default_country }}", verify: "off" } }
              - { id: f_event, key: event, type: hidden, label: "", required: false, default: "{{ params.event_key }}", maps_to: { mode: custom, key: last_attended_event, trait: true } }
      verification_settings: { required: false }
  - ref: form_feedback
    kind: form
    about: A short feedback form linked from the thank-you.
    body:
      name: Event feedback
      slug: event-feedback
      description: "How was {{ params.event_title }}?"
      draft_state:
        version: 1
        meta: { layout: classic, submit_label: Send, success_message: Thank you for telling us. }
        pages:
          - id: p1
            title: "How was {{ params.event_title }}?"
            description: It takes thirty seconds.
            fields:
              - { id: f_rating, key: rating, type: rating, label: "How would you rate {{ params.event_title }}?", required: true, maps_to: { mode: custom, key: event_rating, trait: true }, validation: { min: 1, max: 5 } }
              - { id: f_best, key: best_part, type: long_text, label: What was the best part?, required: false, maps_to: { mode: custom, key: best_part } }
              - { id: f_better, key: do_better, type: long_text, label: What should we do better next time?, required: false, maps_to: { mode: custom, key: do_better } }
              - { id: f_phone, key: phone, type: phone, label: Your phone number (optional), required: false, maps_to: { mode: mapped, slot: profile.phone }, config: { country_picker: true, default_country: "{{ params.default_country }}", verify: "off" } }
      verification_settings: { required: false }

  - ref: seg_registered
    kind: segment
    about: Everyone registered for this event.
    body:
      name: Registered for the event
      description: registered_for is this event's key.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: registered_for, cond_operator: eq, value: "{{ params.event_key }}" }
  - ref: seg_attended
    kind: segment
    about: Registered and checked in.
    body:
      name: Attended the event
      description: Registered for this event and checked in at the door.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: registered_for, cond_operator: eq, value: "{{ params.event_key }}" }
          - { type: trait, trait_key: last_attended_event, cond_operator: eq, value: "{{ params.event_key }}" }
  - ref: seg_missed
    kind: segment
    about: Registered and not checked in.
    body:
      name: Registered, did not attend
      description: Registered for this event and not checked in at the door.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: registered_for, cond_operator: eq, value: "{{ params.event_key }}" }
          - operator: or
            conditions:
              - { type: trait, trait_key: last_attended_event, cond_operator: not_exists }
              - { type: trait, trait_key: last_attended_event, cond_operator: neq, value: "{{ params.event_key }}" }

  - ref: tmpl_confirm_sms
    kind: template
    about: Registration confirmation SMS.
    body:
      name: Event registration confirmed (SMS)
      channel: sms
      category: Events
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "Hi {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, you're registered for {{ params.event_title }} on {{ params.event_when }} at {{ params.event_place }}. See you there! Reply STOP to opt out."
  - ref: tmpl_confirm_email
    kind: template
    about: Registration confirmation email with the details.
    body:
      name: Event registration confirmed (email)
      channel: email
      category: Events
      style: html
      status: active
      subject: "You're registered for {{ params.event_title }}"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "" } }
      variables: {}
      body: >-
        <p>Hi {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>You're registered for {{ params.event_title }}.</p>
        <p><strong>When:</strong> {{ params.event_when }}<br>
        <strong>Where:</strong> {{ params.event_place }}</p>
        <p>When you arrive, tell the door team the phone number you registered with and they'll
        check you in. We'll send you a reminder before the day.</p>
        <p>Questions? Call {{ params.contact_phone }}.</p>
        <p>{{ params.org_name }}</p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>
  - ref: tmpl_week_sms
    kind: template
    about: One-week reminder.
    body:
      name: Event next week (SMS)
      channel: sms
      category: Events
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.event_title }} is next week: {{ params.event_when }} at {{ params.event_place }}. We look forward to seeing you. If your plans change, call {{ params.contact_phone }}. Reply STOP to opt out."
  - ref: tmpl_day_sms
    kind: template
    about: Day-before reminder.
    body:
      name: Event tomorrow (SMS)
      channel: sms
      category: Events
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.event_title }} is tomorrow, {{ params.event_when }}, at {{ params.event_place }}. Tell the door team your phone number to check in. See you there! Reply STOP to opt out."
  - ref: tmpl_thanks_sms
    kind: template
    about: Thank-you SMS with the feedback link.
    body:
      name: Event thank-you (SMS)
      channel: sms
      category: Events
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "Thank you for coming to {{ params.event_title }}, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}! It would help us a lot if you told us how it went: {{ ref.form_feedback.url }} Reply STOP to opt out."
  - ref: tmpl_thanks_email
    kind: template
    about: Thank-you email with the feedback link.
    body:
      name: Event thank-you (email)
      channel: email
      category: Events
      style: html
      status: active
      subject: "Thank you for coming to {{ params.event_title }}"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "" } }
      variables: {}
      body: >-
        <p>Hi {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>Thank you for being at {{ params.event_title }}. It was good to have you.</p>
        <p>Would you tell us how it went? It takes thirty seconds and helps us make the next one
        better. <a href="{{ ref.form_feedback.url }}">Share your feedback</a></p>
        <p>{{ params.org_name }}</p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>
  - ref: tmpl_missed_sms
    kind: template
    about: Kind message to guests who registered and didn't come.
    body:
      name: Sorry we missed you (SMS)
      channel: sms
      category: Events
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "Hi {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, sorry we missed you at {{ params.event_title }}.{% if \"{{ params.catchup_url }}\" != \"\" %} You can catch up here: {{ params.catchup_url }}{% endif %} We'd love to see you next time. Reply STOP to opt out."

  - ref: q_registrations
    kind: saved_question
    about: How many people registered in the period.
    body:
      name: "Report: registrations"
      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_register.id }}" }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: q_checkins
    kind: saved_question
    about: How many people were checked in during the period.
    body:
      name: "Report: check-ins"
      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_checkin.id }}" }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: q_by_ticket
    kind: saved_question
    about: Registrations by ticket type.
    body:
      name: "Report: registrations by ticket type"
      data_plane_id: default
      viz_type: table
      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_register.id }}" }
        aggregations: [{ fn: count, alias: registrations }]
        breakouts: [{ field: properties.ticket_type }]
        limit: 50
  - ref: report_weekly
    kind: report
    about: Registrations and check-ins for the organisers each week. Switch it off after the event.
    body:
      name: "Weekly: event registrations"
      description: Registrations and check-ins in the last 7 days, and how many are registered, checked in and not yet checked in overall.
      schedule_cron: "{{ params.weekly_report_cron }}"
      timezone: "{{ params.timezone }}"
      enabled: false
      spec:
        period: last_7_days
        compare: previous_period
        week_start: monday
        sections:
          - { id: regs, kind: saved_question, title: New registrations, viz: number, params: { question_id: "{{ ref.q_registrations.id }}" } }
          - { id: checkins, kind: saved_question, title: Checked in, viz: number, params: { question_id: "{{ ref.q_checkins.id }}" } }
          - { id: tickets, kind: saved_question, title: Registrations by ticket type, viz: table, params: { question_id: "{{ ref.q_by_ticket.id }}" } }
          - id: sizes
            kind: segment_sizes
            title: Overall
            params:
              segment_ids: ["{{ ref.seg_registered.id }}", "{{ ref.seg_attended.id }}", "{{ ref.seg_missed.id }}"]
      recipients: [{ type: team, id: "{{ ref.team_organisers.id }}" }]
      channel_policy: { mode: email_only }

  - ref: wf_confirm
    kind: workflow
    about: Confirms each registration by SMS and email.
    body:
      name: "Event: registration confirmed"
      description: When the Registration form is submitted, a confirmation SMS and email.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: onboarding.submitted, form_id: "{{ ref.form_register.id }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_confirm_sms.id }}" } } } }
        - { id: s2, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_confirm_email.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 24
      goal: {}
      exit_conditions: []
  - ref: wf_week
    kind: workflow
    about: "Once, a week before: an SMS to everyone registered."
    body:
      name: "Event: one-week reminder"
      description: Once, at the one-week reminder time, an SMS to everyone registered for the event.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_registered.id }}", rrule: FREQ=DAILY;COUNT=1, starts_at: "{{ params.reminder_7d_at }}", timezone: "{{ params.timezone }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_week_sms.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 720
      goal: {}
      exit_conditions: []
  - ref: wf_day
    kind: workflow
    about: "Once, the day before: an SMS to everyone registered."
    body:
      name: "Event: day-before reminder"
      description: Once, at the day-before reminder time, an SMS to everyone registered for the event.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_registered.id }}", rrule: FREQ=DAILY;COUNT=1, starts_at: "{{ params.reminder_1d_at }}", timezone: "{{ params.timezone }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_day_sms.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 720
      goal: {}
      exit_conditions: []
  - ref: wf_thanks
    kind: workflow
    about: "Once, the morning after: thank-you and feedback link to guests who were checked in."
    body:
      name: "Event: thank you and feedback"
      description: Once, the morning after, an SMS and email to guests who were checked in, with the feedback form.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_attended.id }}", rrule: FREQ=DAILY;COUNT=1, starts_at: "{{ params.followup_at }}", timezone: "{{ params.timezone }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_thanks_sms.id }}" } } } }
        - { id: s2, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_thanks_email.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 720
      goal: {}
      exit_conditions: []
  - ref: wf_missed
    kind: workflow
    about: "Once, the morning after: a kind SMS to guests who registered and weren't checked in."
    body:
      name: "Event: sorry we missed you"
      description: Once, the morning after, an SMS to guests who registered and weren't checked in, with a recording or next-event link if you gave one.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_missed.id }}", rrule: FREQ=DAILY;COUNT=1, starts_at: "{{ params.followup_at }}", timezone: "{{ params.timezone }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_missed_sms.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 720
      goal: {}
      exit_conditions: []

eval:
  name: 93_events_registration_and_reminders
  mode: task
  vertical: ""
  prompt: >-
    We're running a workshop on 12 December. Make a registration form with ticket types. Text and
    email people a confirmation when they register. Text everyone a reminder on 5 December and
    again on 11 December. At the door my team will check people in by phone number. On 13
    December, thank the people who came and send them a feedback form, and send the ones who
    didn't a kind message. Send the organisers a weekly count of registrations.
  expect_artifacts: [form, segment, workflow, message_template, report]
  forbid_terms: [discount, cart, checkout]
