# Origamy playbook spec v1. Reference: /playbooks/spec-format/
spec_version: 1
id: edtech.course-completion
title: Keep learners going and celebrate completion
sector: edtech
source:
  kind: synthesized
  notes: >-
    No academy has built this with us yet. Written from the win-back playbook (a segment of
    people who have gone quiet, two messages and a call, leaving the moment they are active
    again) and the thank-you-then-ask pattern of the feedback follow-up. It reads lesson events
    from your learning app. The admissions funnel already lives at edtech.admissions-funnel and
    is not repeated here.
derived_from: base.win-back
vocabulary:
  person: learner
  people: learners
  group: mentors
summary: >-
  Learners who stop doing lessons get a nudge to pick up where they left off, and a call from a
  mentor if they stay away. When someone completes a course they get a congratulations with
  their certificate link, then a review and referral ask two days later. A weekly report shows
  active learners, learners at risk and completions.
when_to_use:
  - You run online courses or a learning app, and many learners start strongly and drift away.
  - Your app sends an event for each lesson completed and another when a course is completed.
  - Mentors or tutors can call learners who are slipping, and you want completions celebrated and turned into reviews.

requires:
  data_in:
    - how: app_events
      what: "Your learning app sends the lesson-completed and course-completed events. The course-completed event can carry a certificate link."
    - how: csv_import
      what: "No developer yet? If your platform exports lesson activity, upload it as Events every week. The nudges then run on that schedule."
  channels:
    - channel: email
      for: The nudge, the congratulations and the review ask
      needs: A verified sending domain for {{ params.email_from }}.
    - channel: sms
      for: The second nudge
      needs: An SMS provider, or Origamy managed SMS after business verification (KYC).
  events:
    - "{{ params.lesson_event }}"
    - "{{ params.completed_event }}"
  traits:
    - mentor_call

parameters:
  - key: org_name
    ask: true
    question: What should messages call your school or platform?
    label: School name
    type: string
    example: Northwind Academy
  - key: portal_url
    ask: true
    question: Where do learners go to continue a course? (your app or course page)
    label: Learning link
    type: url
    example: https://learn.yourschool.example
  - key: mentor_team
    ask: true
    question: Which team calls learners who have gone quiet? (mentors, tutors or one person)
    label: Mentors
    type: team_name
    default: Mentors
  - key: lesson_event
    ask: true
    question: Which event means a learner completed a lesson?
    label: Lesson completed event
    type: event_name
    default: Lesson Completed
  - key: completed_event
    ask: true
    question: Which event means a learner completed a whole course? (it can carry a certificate_url property)
    label: Course completed event
    type: event_name
    default: Course Completed
  - key: review_url
    label: Where learners can leave a review (optional)
    type: url
    default: ""
  - key: referral_url
    label: Where learners can invite a friend (optional)
    type: url
    default: ""
  - key: call_due_hours
    label: Hours a mentor has to make the call
    type: hours
    default: 48
  - key: email_from
    label: Sender address
    type: email
    default: "{{ workspace.default_sender }}"
  - key: timezone
    label: Time zone for the report
    type: timezone
    default: "{{ workspace.timezone }}"
  - key: report_cron
    label: Weekly learning report
    type: cron
    default: "0 8 * * 1"
  - key: greeting_fallback
    label: Greeting when a first name is missing
    type: string
    default: there

timings:
  - "7 days with no lesson (after lessons in the last 30 days): a \"pick up where you left off\" email, then an SMS 3 days later if they are still away. It stops as soon as they do a lesson."
  - "14 days with no lesson: a call task for {{ params.mentor_team }}, due in {{ params.call_due_hours }} hours."
  - "Course completed: the congratulations and certificate link straight away, then a review and referral ask 2 days later."
  - "Weekly learning report: {{ params.report_cron }} in {{ params.timezone }}."

customer_must:
  - "Send {{ params.lesson_event }} for every lesson and {{ params.completed_event }} for every course, with the learner's email and phone as traits. If the course-completed event has a certificate_url property, the congratulations links to it. Otherwise it links to {{ params.portal_url }}."
  - Add people to the {{ params.mentor_team }} team. A task assigned to an empty team reaches nobody.
  - Give each lesson event a stable messageId per occurrence, so retries are dropped.
  - Only add a review or referral link if you want one. Leave both blank to send the thank-you without an ask.
  - Verify the sending domain, connect an SMS provider or finish business verification, switch the follow-ups and report on, and test with your own account.

objects:
  - ref: team_mentors
    kind: team
    about: Calls learners who have gone quiet; gets the weekly report.
    body: { name: "{{ params.mentor_team }}" }

  - ref: seg_quiet
    kind: segment
    about: Did a lesson in the last 30 days, none in the last 7, and hasn't finished a course lately.
    body:
      name: Learners gone quiet
      description: "{{ params.lesson_event }} in the last 30 days but not in the last 7, and no {{ params.completed_event }} in the last 90."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: event, event_name: "{{ params.lesson_event }}", cond_operator: has_done, time_window: 30d }
          - { type: event, event_name: "{{ params.lesson_event }}", cond_operator: has_not_done, time_window: 7d }
          - { type: event, event_name: "{{ params.completed_event }}", cond_operator: has_not_done, time_window: 90d }
  - ref: seg_at_risk
    kind: segment
    about: Did a lesson in the last 30 days, none in the last 14, and hasn't finished a course lately.
    body:
      name: Learners at risk
      description: "{{ params.lesson_event }} in the last 30 days but not in the last 14, and no {{ params.completed_event }} in the last 90."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: event, event_name: "{{ params.lesson_event }}", cond_operator: has_done, time_window: 30d }
          - { type: event, event_name: "{{ params.lesson_event }}", cond_operator: has_not_done, time_window: 14d }
          - { type: event, event_name: "{{ params.completed_event }}", cond_operator: has_not_done, time_window: 90d }
  - ref: seg_active
    kind: segment
    about: Did a lesson in the last 7 days.
    body:
      name: Active learners
      description: "{{ params.lesson_event }} in the last 7 days."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: event, event_name: "{{ params.lesson_event }}", cond_operator: has_done, time_window: 7d }

  - ref: tmpl_nudge_email
    kind: template
    about: Pick up where you left off.
    body:
      name: Pick up where you left off (email)
      channel: email
      category: Learning
      style: html
      status: active
      subject: "Your next lesson is waiting, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "Ten minutes is enough to get going again." } }
      variables: {}
      body: >-
        <p>Hi {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>You were making good progress with {{ params.org_name }}, and we'd hate for you to lose
        your momentum. Even ten minutes today will get you going again.</p>
        <p><a href="{{ params.portal_url }}">Pick up where you left off</a></p>
        <p>If something is in the way, reply and tell us. A real person will help.</p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>
  - ref: tmpl_nudge_sms
    kind: template
    about: Second nudge by SMS.
    body:
      name: Pick up where you left off (SMS)
      channel: sms
      category: Learning
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: we miss you, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}. Your next lesson is ready whenever you are: {{ params.portal_url }} Reply STOP to opt out."
  - ref: tmpl_congrats
    kind: template
    about: Congratulations with the certificate link.
    body:
      name: Course completed (email)
      channel: email
      category: Learning
      style: html
      status: active
      subject: "Congratulations, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}! You finished {{ properties.course_name | default: \"your course\" }}"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "Your certificate is ready." } }
      variables: {}
      body: >-
        <p>Hi {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>You did it! You've completed {{ properties.course_name | default: "your course" }} with
        {{ params.org_name }}. We're proud of you.</p>
        <p><a href="{{ properties.certificate_url | default: "{{ params.portal_url }}" }}">Get your certificate</a></p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>
  - ref: tmpl_ask
    kind: template
    about: Review and referral ask two days later. Each link only shows if you set it.
    body:
      name: Review and share (email)
      channel: email
      category: Learning
      style: html
      status: active
      subject: "How was your course, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}?"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "Two minutes to help the next learner." } }
      variables: {}
      body: >-
        <p>Hi {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>Congratulations again on finishing. Your view helps the next learner choose well.</p>
        {% if "{{ params.review_url }}" != "" %}<p><a href="{{ params.review_url }}">Leave a short review</a></p>{% endif %}
        {% if "{{ params.referral_url }}" != "" %}<p>Know someone who'd enjoy it? <a href="{{ params.referral_url }}">Invite a friend</a></p>{% endif %}
        <p>Ready for what's next? <a href="{{ params.portal_url }}">See more courses</a></p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>

  - ref: q_active
    kind: saved_question
    about: Learners who did a lesson in the report's period.
    body:
      name: "Report: active learners"
      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.lesson_event }}" }] }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: q_completions
    kind: saved_question
    about: Learners who completed a course in the report's period.
    body:
      name: "Report: course completions"
      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.completed_event }}" }] }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: report_weekly
    kind: report
    about: The week's active learners and completions, how many are quiet or at risk, and the state of the mentor calls.
    body:
      name: "Weekly: learners"
      description: Last week's active learners and completions, and who is slipping away.
      schedule_cron: "{{ params.report_cron }}"
      timezone: "{{ params.timezone }}"
      enabled: false
      spec:
        period: last_7_days
        compare: previous_period
        week_start: monday
        sections:
          - { id: active, kind: saved_question, title: Active learners, viz: number, params: { question_id: "{{ ref.q_active.id }}" } }
          - { id: done, kind: saved_question, title: Course completions, viz: number, params: { question_id: "{{ ref.q_completions.id }}" } }
          - id: risk
            kind: segment_sizes
            title: Gone quiet and at risk
            params: { segment_ids: ["{{ ref.seg_quiet.id }}", "{{ ref.seg_at_risk.id }}"] }
          - { id: tasks, kind: tasks_summary, title: Mentor calls, params: {} }
      recipients: [{ type: team, id: "{{ ref.team_mentors.id }}" }]
      channel_policy: { mode: email_only }

  - ref: wf_nudge
    kind: workflow
    about: Email, then SMS three days later, for learners who go quiet; stops when they do a lesson.
    body:
      name: "Learning 1: pick up where you left off"
      description: A nudge email and then an SMS for learners with no lesson in a week; stops on any lesson.
      enabled: false
      trigger_type: segment_enter
      trigger_config: { segment_id: "{{ ref.seg_quiet.id }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_nudge_email.id }}" } } } }
        - { id: s2, type: delay, config: { duration: 3d } }
        - { id: s3, type: filter, config: { condition: { type: segment, cond_operator: member_of, value: "{{ ref.seg_quiet.id }}" } } }
        - { id: s4, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_nudge_sms.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 720
      goal: { event_name: "{{ params.lesson_event }}", attribution_window_hours: 168 }
      exit_conditions: [{ event_name: "{{ params.lesson_event }}" }]
  - ref: wf_mentor
    kind: workflow
    about: A mentor call task for learners at risk; the outcome is saved.
    body:
      name: "Learning 2: mentor call"
      description: A call task for the mentors when a learner has been away two weeks.
      enabled: false
      trigger_type: segment_enter
      trigger_config: { segment_id: "{{ ref.seg_at_risk.id }}" }
      steps:
        - id: s1
          type: human_task
          config:
            human_task:
              title: "Learner slipping: {{ traits.first_name }} {{ traits.last_name }}"
              instructions: |-
                {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) was learning with us but hasn't done a lesson in two weeks.

                Call or message to ask how they are doing and what is in the way. Offer practical help: a new schedule, a catch-up session, a different course. Choose an outcome and add a short note.
              assignee_refs: [{ type: team, id: "{{ ref.team_mentors.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: back_on_track, label: Back on track, intent: positive }
                - { id: needs_time, label: Needs more time, intent: neutral }
                - { id: dropping_out, label: Dropping out, intent: negative }
                - { id: not_reached, label: Could not reach, intent: negative }
              due_hours: "{{ params.call_due_hours }}"
              wait: true
              wait_timeout_hours: 168
              notify: immediate
        - { id: s2, type: action, config: { action_type: set_trait, traits: { mentor_call: "{{ properties.task_outcome | default: \"unknown\" }}", mentor_call_note: "{{ properties.task_note }}" } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 720
      goal: { event_name: "{{ params.lesson_event }}", attribution_window_hours: 168 }
      exit_conditions: []
  - ref: wf_completed
    kind: workflow
    about: Congratulations and the certificate link, then a review and referral ask two days later.
    body:
      name: "Learning 3: course completed"
      description: Congratulations and certificate link, then a review and referral ask 2 days later.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: "{{ params.completed_event }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_congrats.id }}" } } } }
        - { id: s2, type: delay, config: { duration: 2d } }
        - { id: s3, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_ask.id }}" } } } }
      re_entry_policy: every_time
      re_entry_cooldown_hours: 0
      goal: {}
      exit_conditions: []

eval:
  name: 108_edtech_course_completion
  mode: task
  vertical: ""
  prompt: >-
    Our learning app sends Lesson Completed and Course Completed events. If a learner has done
    lessons recently but nothing for a week, email them to pick up where they left off, and text
    them three days later if they're still away, stopping when they do a lesson. After two weeks
    away, give our mentors a task to call them. When someone completes a course, email
    congratulations with their certificate link, then two days later ask for a review and a
    referral. Send our mentors a weekly report of active learners, learners at risk and
    completions.
  expect_artifacts: [segment, workflow, message_template, report]
  forbid_terms: []
