# Origamy playbook spec v1. Reference: /playbooks/spec-format/
spec_version: 1
id: edtech.admissions-funnel
title: Admissions funnel
sector: edtech
source:
  kind: engagement
  date: 2026-10-25
  ref: eng-2026-10-schools-a
  notes: >-
    An online school's admissions tracking, set up 2026-10-17 to 2026-10-25: four funnel events
    sent from its backend, a stage trait set before each event, and an event-named trait that
    holds the event's properties so segments can filter on them. The reminders and the
    admissions call are written from those shapes; the school had not built them yet.
derived_from: null
vocabulary:
  person: applicant
  people: applicants
  group: admissions team
summary: >-
  Follows applicants from sign-up to paid: a nudge for people who signed up but never applied,
  reminders and then a call from admissions for people who applied but haven't paid, a
  next-steps email once they pay, a funnel on a dashboard and a weekly admissions report.
when_to_use:
  - Applicants sign up, apply and pay an application or acceptance fee online.
  - Your backend can send events at each step (the Node, Go and PHP SDKs, or the HTTP API).
  - An admissions team can call applicants who stall before paying.

requires:
  data_in:
    - how: app_events
      what: "Your admissions portal sends the four funnel events and sets the stage trait."
  channels:
    - channel: email
      for: Nudges, reminders and next steps
      needs: A verified sending domain for {{ params.email_from }}. On a self-hosted (BYOD) data plane, your own email connection.
    - channel: sms
      for: The last payment reminder
      needs: An SMS provider, or Origamy managed SMS after business verification (KYC).
  events:
    - "{{ params.signup_event }}"
    - "{{ params.application_event }}"
    - "{{ params.checkout_event }}"
    - "{{ params.payment_event }}"
  traits:
    - "{{ params.stage_trait }}"   # application_submitted → checkout_initiated → paid_application

parameters:
  - key: org_name
    ask: true
    question: What should messages call your school?
    label: School name
    type: string
    example: Northwind Academy
  - key: application_url
    ask: true
    question: Where do applicants apply or pay? (a link to your portal)
    label: Application link
    type: url
    example: https://apply.yourschool.example
  - key: admissions_team
    ask: true
    question: Which team calls applicants who haven't paid?
    label: Admissions team
    type: team_name
    default: Admissions
  - key: escalation_team
    ask: true
    question: Who should hear if admissions doesn't call in time?
    label: Escalation team
    type: team_name
    default: Admissions leads
  - key: email_from
    ask: true
    question: Which address should emails come from?
    label: Sender address
    type: email
    example: admissions@yourschool.example
  - key: signup_event
    label: Sign-up event
    type: event_name
    default: Signed Up
  - key: application_event
    label: Application submitted (unpaid) event
    type: event_name
    default: Application Submitted
  - key: checkout_event
    label: Checkout started event
    type: event_name
    default: Checkout Started
  - key: payment_event
    label: Payment succeeded event
    type: event_name
    default: Payment Successful
  - key: stage_trait
    label: Stage trait your backend sets
    type: trait_key
    default: lead_status
  - key: timezone
    label: Time zone for the report
    type: timezone
    default: "{{ workspace.timezone }}"
  - key: greeting_fallback
    label: Greeting when a first name is missing
    type: string
    default: there
  - key: report_cron
    label: Weekly admissions report
    type: cron
    default: "0 8 * * 1"

timings:
  - "Signed up, no application: a nudge after 1 day and another 3 days later; it stops as soon as they apply."
  - "Applied, not paid: an email reminder after 1 day, an SMS 2 days later, and a call task for admissions 1 day after that, due in 48 hours and escalated after 48. Each step is skipped once they've paid."
  - Paid applicants get the next-steps email straight away.
  - "Weekly admissions report: {{ params.report_cron }} in {{ params.timezone }}."

customer_must:
  - "Send the four events from your backend, and set {{ params.stage_trait }} with an identify call before each one, so a follow-up starting on the event sees the new stage: application_submitted, checkout_initiated, then paid_application. Only application-fee steps should move the stage, so a paid applicant never drops back."
  - Give each event a stable messageId per occurrence (for example payment_successful:<payment reference>), so webhook retries are dropped.
  - Send the applicant's country with their phone number, so the number is stored in international format.
  - Add people to the {{ params.admissions_team }} and {{ params.escalation_team }} teams.
  - Verify the sending domain; for the SMS, connect a provider or finish business verification.
  - Switch the follow-ups and report on (a follow-up only runs once it's on), run one test applicant through with your own phone and email, check every message, then erase the test applicant.

objects:
  - ref: team_admissions
    kind: team
    about: Calls applicants who haven't paid; gets the weekly report.
    body: { name: "{{ params.admissions_team }}" }
  - ref: team_escalation
    kind: team
    about: Hears about calls not made in time.
    body: { name: "{{ params.escalation_team }}" }

  - ref: seg_no_application
    kind: segment
    about: Signed up in the last 30 days but never applied.
    body:
      name: Signed up, no application
      description: Signed up in the last 30 days and has no admissions stage yet.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: event, event_name: "{{ params.signup_event }}", cond_operator: has_done, time_window: 30d }
          - { type: trait, trait_key: "{{ params.stage_trait }}", cond_operator: not_exists }
  - ref: seg_unpaid
    kind: segment
    about: Applied (or started checkout) but hasn't paid.
    body:
      name: Applied, not paid
      description: Stage is application_submitted or checkout_initiated.
      segment_type: dynamic
      status: active
      definition:
        operator: or
        conditions:
          - { type: trait, trait_key: "{{ params.stage_trait }}", cond_operator: eq, value: application_submitted }
          - { type: trait, trait_key: "{{ params.stage_trait }}", cond_operator: eq, value: checkout_initiated }
  - ref: seg_paid
    kind: segment
    about: Paid the application fee.
    body:
      name: Paid applicants
      description: Stage is paid_application.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.stage_trait }}", cond_operator: eq, value: paid_application }

  - ref: tmpl_apply_nudge
    kind: template
    about: Nudge to start an application.
    body:
      name: Start your application (email)
      channel: email
      category: Admissions
      style: html
      status: active
      subject: "Your place at {{ params.org_name }} is waiting, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "It only takes a few minutes." } }
      variables: {}
      body: >-
        <p>Hi {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>Thanks for signing up with {{ params.org_name }}. Your application takes a few minutes,
        and you can save it and come back.</p>
        <p><a href="{{ params.application_url }}">Start your application</a></p>
        <p>Questions? Just reply to this email.</p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>
  - ref: tmpl_pay_email
    kind: template
    about: Reminder to complete payment.
    body:
      name: Complete your application (email)
      channel: email
      category: Admissions
      style: html
      status: active
      subject: "One step left on your {{ params.org_name }} application"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "Complete your payment to secure your place." } }
      variables: {}
      body: >-
        <p>Hi {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>We've received your application. Complete the payment to secure your place.</p>
        <p><a href="{{ params.application_url }}">Complete your application</a></p>
        <p>If something went wrong at checkout, reply and our admissions team will help.</p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>
  - ref: tmpl_pay_sms
    kind: template
    about: Last payment reminder by SMS.
    body:
      name: Complete your application (SMS)
      channel: sms
      category: Admissions
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "Hi {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, your {{ params.org_name }} application is one step from done. Complete it here: {{ params.application_url }} Reply STOP to opt out."
  - ref: tmpl_next_steps
    kind: template
    about: Next steps after payment.
    body:
      name: Payment received, next steps (email)
      channel: email
      category: Admissions
      style: html
      status: active
      subject: "You're in, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}: your next steps"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "Here's what happens next." } }
      variables: {}
      body: >-
        <p>Hi {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>We've received your payment. Welcome to {{ params.org_name }}! Here's what happens next,
        and our admissions team is a reply away if you need anything.</p>
        <p><a href="{{ params.application_url }}">Go to your portal</a></p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>

  - ref: q_applications
    kind: saved_question
    about: Applications in the report's period.
    body:
      name: "Report: applications"
      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: "{{ params.application_event }}" }] }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: q_paid
    kind: saved_question
    about: Paid applicants in the report's period.
    body:
      name: "Report: paid applicants"
      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: "{{ params.payment_event }}" }] }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: q_signups
    kind: saved_question
    about: Sign-ups in the report's period.
    body:
      name: "Report: sign-ups"
      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: "{{ params.signup_event }}" }] }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: funnel_admissions
    kind: saved_funnel
    about: Sign-up → application → checkout → paid, within 30 days.
    body:
      name: Admissions funnel
      data_plane_id: default
      visibility: workspace
      definition:
        range: 90d
        window: 2592000
        view: steps
        breakdown: ""
        interval: ""
        compare: false
        steps:
          - { type: track, name: "{{ params.signup_event }}", filters: [] }
          - { type: track, name: "{{ params.application_event }}", filters: [] }
          - { type: track, name: "{{ params.checkout_event }}", filters: [] }
          - { type: track, name: "{{ params.payment_event }}", filters: [] }
  - ref: dash_admissions
    kind: dashboard
    about: The funnel and this period's numbers.
    body:
      name: Admissions
      description: Sign-ups, applications and payments.
      visibility: workspace
      cards:
        - { saved_funnel_id: "{{ ref.funnel_admissions.id }}", x: 0, y: 0, w: 12, h: 6 }
        - { saved_question_id: "{{ ref.q_signups.id }}", viz_type: number, x: 0, y: 6, w: 4, h: 3 }
        - { saved_question_id: "{{ ref.q_applications.id }}", viz_type: number, x: 4, y: 6, w: 4, h: 3 }
        - { saved_question_id: "{{ ref.q_paid.id }}", viz_type: number, x: 8, y: 6, w: 4, h: 3 }
  - ref: report_weekly
    kind: report
    about: The week's sign-ups, applications and payments, and where applicants stand now.
    body:
      name: "Weekly: admissions"
      description: Last week's sign-ups, applications and payments, compared with the week before.
      schedule_cron: "{{ params.report_cron }}"
      timezone: "{{ params.timezone }}"
      enabled: false
      spec:
        period: last_7_days
        compare: previous_period
        week_start: monday
        sections:
          - { id: signups, kind: saved_question, title: Sign-ups, viz: number, params: { question_id: "{{ ref.q_signups.id }}" } }
          - { id: apps, kind: saved_question, title: Applications, viz: number, params: { question_id: "{{ ref.q_applications.id }}" } }
          - { id: paid, kind: saved_question, title: Paid, viz: number, params: { question_id: "{{ ref.q_paid.id }}" } }
          - id: stages
            kind: segment_sizes
            title: Where applicants are now
            params: { segment_ids: ["{{ ref.seg_no_application.id }}", "{{ ref.seg_unpaid.id }}", "{{ ref.seg_paid.id }}"] }
          - { id: tasks, kind: tasks_summary, title: Admissions calls, params: {} }
          - { id: link, kind: text, params: { body: "Funnel: {{ ref.dash_admissions.url }}" } }
      recipients: [{ type: team, id: "{{ ref.team_admissions.id }}" }]
      channel_policy: { mode: email_only }

  - ref: wf_apply_nudge
    kind: workflow
    about: Signed up but didn't apply? Two nudges, stopping once they apply.
    body:
      name: "Admissions 1: start your application"
      description: After sign-up, two nudges to apply, skipped once the applicant has a stage.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: "{{ params.signup_event }}" }
      steps:
        - { id: s1, type: delay, config: { duration: 1d } }
        - { id: s2, type: filter, config: { condition: { type: trait, trait_key: "{{ params.stage_trait }}", cond_operator: not_exists } } }
        - { id: s3, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_apply_nudge.id }}" } } } }
        - { id: s4, type: delay, config: { duration: 3d } }
        - { id: s5, type: filter, config: { condition: { type: trait, trait_key: "{{ params.stage_trait }}", cond_operator: not_exists } } }
        - { id: s6, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_apply_nudge.id }}" } } } }
      re_entry_policy: once
      re_entry_cooldown_hours: 24
      goal: {}
      exit_conditions: []
  - ref: wf_pay_reminder
    kind: workflow
    about: Applied but didn't pay? Email, then SMS, then a call from admissions; each step skipped once they pay.
    body:
      name: "Admissions 2: complete your application"
      description: After an unpaid application, an email, an SMS and an admissions call, each skipped once the applicant has paid.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: "{{ params.application_event }}" }
      steps:
        - { id: s1, type: delay, config: { duration: 1d } }
        - { id: s2, type: filter, config: { condition: { type: trait, trait_key: "{{ params.stage_trait }}", cond_operator: neq, value: paid_application } } }
        - { id: s3, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_pay_email.id }}" } } } }
        - { id: s4, type: delay, config: { duration: 2d } }
        - { id: s5, type: filter, config: { condition: { type: trait, trait_key: "{{ params.stage_trait }}", cond_operator: neq, value: paid_application } } }
        - { id: s6, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_pay_sms.id }}" } } } }
        - { id: s7, type: delay, config: { duration: 1d } }
        - { id: s8, type: filter, config: { condition: { type: trait, trait_key: "{{ params.stage_trait }}", cond_operator: neq, value: paid_application } } }
        - id: s9
          type: human_task
          config:
            human_task:
              title: "Call applicant: {{ traits.first_name }} {{ traits.last_name }}"
              instructions: |-
                {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) applied 4 days ago and hasn't paid.

                Call to help them finish, then choose an outcome and add a note.
              assignee_refs: [{ type: team, id: "{{ ref.team_admissions.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: will_pay, label: Will complete, intent: positive }
                - { id: not_now, label: Not this intake, intent: neutral }
                - { id: not_reached, label: Could not reach, intent: negative }
              due_hours: 48
              wait: true
              wait_timeout_hours: 168
              notify: immediate
              escalation_ladder:
                - { after_hours: 48, to: [{ type: team, id: "{{ ref.team_escalation.id }}" }] }
        - { id: s10, type: action, config: { action_type: set_trait, traits: { admissions_call: "{{ properties.task_outcome | default: \"unknown\" }}", admissions_call_note: "{{ properties.task_note }}" } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 720
      goal: {}
      exit_conditions: []
  - ref: wf_next_steps
    kind: workflow
    about: Next-steps email when the payment comes in.
    body:
      name: "Admissions 3: payment received"
      description: The next-steps email after the application payment.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: "{{ params.payment_event }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_next_steps.id }}" } } } }
      re_entry_policy: once
      re_entry_cooldown_hours: 24
      goal: {}
      exit_conditions: []

eval:
  name: 62_edtech_admissions_funnel
  mode: task
  vertical: ""
  prompt: >-
    We're an online school. Our backend sends Signed Up, Application Submitted, Checkout
    Started and Payment Successful, and sets lead_status. Nudge people who signed up but never
    applied, remind applicants who haven't paid by email then SMS, and if they still haven't
    paid have admissions call them. Send a next-steps email after payment, and email the
    admissions team a weekly funnel report.
  expect_artifacts: [segment, workflow, message_template, report]
  forbid_terms: []
