# Origamy playbook spec v1. Reference: /playbooks/spec-format/
spec_version: 1
id: nonprofits.donor-thank-you
title: Donor thank-you and yearly follow-up
sector: nonprofits
source:
  kind: synthesized
  notes: >-
    No charity has built this with us yet. Modelled on the church giving thank-you playbook
    (same first-gift task and no-amounts rule), with the gift recorded by a staff form instead
    of a giving platform event, and date rules for the yearly and lapsed messages from the
    payment-reminder pattern. Amounts are never asked for, stored or shown.
derived_from: church.giving-thank-you
vocabulary:
  person: donor
  people: donors
  group: team
summary: >-
  Your team records each gift on a short form. The donor gets a warm thank-you by SMS and email
  within a day, never mentioning the amount, and first-time donors get a personal call. About a
  year after their last gift they hear what their support did and are invited to give again. A
  monthly report counts donors and first-time donors.
when_to_use:
  - An NGO, foundation, school alumni fund or charity arm takes gifts by bank transfer, cash, cheque, payment link or at events, and someone records them.
  - Donors are thanked late or not at all, and first-time donors drift away.
  - Your funders or board want to see how many people gave, not only how much.

requires:
  data_in:
    - how: staff_form
      what: "A Record a donation form your team fills when a gift arrives, matched to the donor by phone. It never asks for the amount."
    - how: csv_import
      what: "Optional: import past donors with their last gift date as last_donation_at, so the yearly and lapsed messages work from day one."
  channels:
    - channel: sms
      for: The thank-you and the yearly reminder
      needs: An SMS provider, or Origamy managed SMS after business verification (KYC).
    - channel: email
      for: The thank-you, the yearly impact email, the lapsed-donor email, the monthly report
      needs: A verified sending domain for {{ params.email_from }}.
  events:
    - onboarding.submitted
  traits:
    - last_donation_at
    - last_campaign
    - first_gift_call

parameters:
  - key: org_name
    ask: true
    question: What should messages call your organisation?
    label: Organisation name
    type: string
    example: Northwind Foundation
  - key: impact_line
    ask: true
    question: "Finish this sentence in your own words: \"Your gift helps us ...\""
    help: One short line about what donations make possible. No amounts or numbers of people.
    label: What gifts make possible
    type: text
    example: keep our after-school centre open and our children fed
  - key: campaigns
    ask: true
    question: Which campaigns or funds can a gift be recorded against?
    help: One per line. Include a general one.
    label: Campaigns
    type: list
    default: [General fund, Education, Emergency relief]
  - key: director_team
    ask: true
    question: Which team thanks first-time donors personally? (it can be just you)
    label: Thank-you team
    type: team_name
    default: Directors
  - key: donate_url
    ask: true
    question: Where can people give again? (a payment link or your donate page, or leave empty)
    label: Donate link
    type: url
    default: ""
  - key: report_team
    label: Team that gets the monthly report
    type: team_name
    default: Fundraising
  - key: email_from
    label: Sender address
    type: email
    default: "{{ workspace.default_sender }}"
  - key: thanks_delay
    label: Thank-you after the gift is recorded
    type: duration
    default: 4h
  - key: call_due_hours
    label: Hours to thank a first-time donor personally
    type: hours
    default: 72
  - key: send_time
    label: Daily send time for the yearly and lapsed messages
    type: time
    default: "09:00"
  - key: report_cron
    label: Monthly report (five-field cron, in the time zone below)
    type: cron
    default: "0 9 2 * *"
  - key: timezone
    label: Time zone
    type: timezone
    default: "{{ workspace.timezone }}"
  - key: default_country
    label: Default phone country on the form
    type: country_code
    default: "{{ workspace.country }}"
  - key: greeting_fallback
    label: Greeting when a first name is missing
    type: string
    default: friend

timings:
  - Thank-you by SMS and email {{ params.thanks_delay }} after a gift is recorded, at most once a day per donor.
  - First-time donors also create a task for {{ params.director_team }} to thank them personally, due in {{ params.call_due_hours }} hours.
  - Every day at {{ params.send_time }} ({{ params.timezone }}), donors whose last gift was between 330 and 365 days ago get the yearly email and SMS, at most once every 300 days.
  - The same daily run sends one email to donors whose last gift was 18 to 30 months ago.
  - Monthly report on the schedule {{ params.report_cron }}.

customer_must:
  - "Fill the Record a donation form for every gift, the same day if you can: the donor's phone, first name, whether it is their first gift, the campaign and the date. It deliberately has no amount. Keep amounts in your own books."
  - Add people to the {{ params.director_team }} and {{ params.report_team }} teams.
  - Verify the sending domain, connect an SMS provider or finish business verification, then switch the follow-ups and the report on. Test with your own phone and a test gift.
  - Check you have the donor's permission to message them. Tell donors on your receipts or donate page that you will thank them by SMS or email.
  - "To start the yearly and lapsed messages from day one, import past donors as a CSV with last_donation_at as an ISO date, e.g. 2026-01-15."

objects:
  - ref: team_directors
    kind: team
    about: Thanks first-time donors personally.
    body: { name: "{{ params.director_team }}" }
  - ref: team_report
    kind: team
    about: Gets the monthly donor report.
    body: { name: "{{ params.report_team }}" }

  - ref: form_record
    kind: form
    about: Staff form to record a gift. It has no amount. It saves the gift date and campaign on the donor.
    body:
      name: Record a donation
      slug: record-a-donation
      description: Staff only. One entry per gift. Do not enter amounts here.
      draft_state:
        version: 1
        meta: { layout: classic, submit_label: Record gift, success_message: Recorded. Thank you. }
        pages:
          - id: p1
            title: Record a donation
            description: Use the donor's phone number each time so their gifts stay together. Do not enter the amount.
            fields:
              - { id: f_phone, key: phone, type: phone, label: Donor'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: Donor's first name, required: true, maps_to: { mode: custom, key: first_name, trait: true } }
              - { id: f_email, key: email, type: email, label: Donor's email (if they gave it), required: false, maps_to: { mode: mapped, slot: profile.email } }
              - { id: f_date, key: gift_date, type: date, label: Date of the gift, required: true, maps_to: { mode: custom, key: last_donation_at, trait: true }, config: { default_today: true } }
              - id: f_first_gift
                key: first_gift
                type: multiple_choice
                label: Is this their first gift to us?
                required: true
                maps_to: { mode: custom, key: first_gift }
                options: [{ value: "yes", label: "Yes" }, { value: "no", label: "No" }]
              - id: f_campaign
                key: campaign
                type: single_select
                label: Which campaign or fund?
                required: true
                maps_to: { mode: custom, key: last_campaign, trait: true }
                options:
                  - $each: params.campaigns
                    $value: { value: "{{ item }}", label: "{{ item }}" }
      verification_settings: { required: false }

  - ref: seg_year
    kind: segment
    about: Last gift 330 to 365 days ago.
    body:
      name: A year since the last gift
      description: last_donation_at between 330 and 365 days ago.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: last_donation_at, cond_operator: older_than, value: 330d }
          - { type: trait, trait_key: last_donation_at, cond_operator: within_last, value: 365d }
  - ref: seg_lapsed
    kind: segment
    about: Last gift 18 to 30 months ago.
    body:
      name: Lapsed donors
      description: last_donation_at between 540 and 900 days ago.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: last_donation_at, cond_operator: older_than, value: 540d }
          - { type: trait, trait_key: last_donation_at, cond_operator: within_last, value: 900d }
  - ref: seg_active
    kind: segment
    about: Gave in the last 12 months (for the report).
    body:
      name: Donors in the last 12 months
      description: last_donation_at within the last 365 days.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: last_donation_at, cond_operator: within_last, value: 365d }

  - ref: tmpl_thanks_email
    kind: template
    about: Thank-you email (no amounts).
    body:
      name: Thank you for your gift (email)
      channel: email
      category: Donors
      style: html
      status: active
      subject: "Thank you, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "" } }
      variables: {}
      body: >-
        <p>Dear {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>Thank you for your gift to {{ params.org_name }}. Your gift helps us {{ params.impact_line }}.</p>
        <p>We are grateful for you.</p>
        <p>With thanks,<br>{{ params.org_name }}</p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>
  - ref: tmpl_thanks_sms
    kind: template
    about: Thank-you SMS (no amounts).
    body:
      name: Thank you for your gift (SMS)
      channel: sms
      category: Donors
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "Dear {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, thank you for your gift to {{ params.org_name }}. We are grateful for you. Reply STOP to opt out."
  - ref: tmpl_year_email
    kind: template
    about: Yearly impact email with a gentle invitation to give again.
    body:
      name: A year of your support (email)
      channel: email
      category: Donors
      style: html
      status: active
      subject: "A year of your support, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "" } }
      variables: {}
      body: >-
        <p>Dear {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>It has been about a year since your last gift to {{ params.org_name }}. Thank you for
        standing with us. Your support helps us {{ params.impact_line }}.</p>
        {% if "{{ params.donate_url }}" != "" %}<p>If you would like to give again this year, you can do it here:
        <a href="{{ params.donate_url }}">Give again</a></p>{% endif %}
        <p>Whatever you decide, thank you.</p>
        <p>With gratitude,<br>{{ params.org_name }}</p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>
  - ref: tmpl_year_sms
    kind: template
    about: Yearly SMS.
    body:
      name: A year of your support (SMS)
      channel: sms
      category: Donors
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "Dear {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, it's been a year since your last gift to {{ params.org_name }}. Thank you for standing with us.{% if \"{{ params.donate_url }}\" != \"\" %} To give again: {{ params.donate_url }}{% endif %} Reply STOP to opt out."
  - ref: tmpl_lapsed_email
    kind: template
    about: One warm email to donors who haven't given for a while.
    body:
      name: We'd love to stay in touch (email)
      channel: email
      category: Donors
      style: html
      status: active
      subject: "We'd love to stay in touch, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "" } }
      variables: {}
      body: >-
        <p>Dear {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>It has been a while since we last heard from you, and we wanted to say thank you for
        what you gave to {{ params.org_name }}. Because of supporters like you, we are able to
        {{ params.impact_line }}.</p>
        {% if "{{ params.donate_url }}" != "" %}<p>If you would like to support us again, here is how:
        <a href="{{ params.donate_url }}">Give</a></p>{% endif %}
        <p>If you'd rather not hear from us, you can unsubscribe below and we will respect that.</p>
        <p>With thanks,<br>{{ params.org_name }}</p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>

  - ref: q_gifts
    kind: saved_question
    about: How many gifts were recorded in the period.
    body:
      name: "Report: gifts 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_record.id }}" }
        aggregations: [{ fn: count, alias: gifts }]
  - ref: q_donors
    kind: saved_question
    about: How many different people gave in the period.
    body:
      name: "Report: donors"
      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_record.id }}" }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: q_first_donors
    kind: saved_question
    about: How many people gave for the first time.
    body:
      name: "Report: first-time donors"
      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_record.id }}" }
            - { field: properties.first_gift, op: eq, value: "yes" }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: q_by_campaign
    kind: saved_question
    about: Gifts recorded per campaign.
    body:
      name: "Report: gifts by campaign"
      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_record.id }}" }
        aggregations: [{ fn: count, alias: gifts }]
        breakouts: [{ field: properties.campaign }]
        limit: 50
  - ref: report_monthly
    kind: report
    about: Donors and first-time donors last month, by campaign. Counts only, no amounts.
    body:
      name: "Monthly: donor summary"
      description: Number of gifts, donors and first-time donors last month, and how many gave in the last year. No amounts.
      schedule_cron: "{{ params.report_cron }}"
      timezone: "{{ params.timezone }}"
      enabled: false
      spec:
        period: previous_month
        compare: previous_period
        week_start: monday
        sections:
          - { id: gifts, kind: saved_question, title: Gifts recorded, viz: number, params: { question_id: "{{ ref.q_gifts.id }}" } }
          - { id: donors, kind: saved_question, title: Donors, viz: number, params: { question_id: "{{ ref.q_donors.id }}" } }
          - { id: first, kind: saved_question, title: First-time donors, viz: number, params: { question_id: "{{ ref.q_first_donors.id }}" } }
          - { id: campaigns, kind: saved_question, title: Gifts by campaign, viz: table, params: { question_id: "{{ ref.q_by_campaign.id }}" } }
          - id: sizes
            kind: segment_sizes
            title: Where donors are now
            params:
              segment_ids: ["{{ ref.seg_active.id }}", "{{ ref.seg_year.id }}", "{{ ref.seg_lapsed.id }}"]
          - { id: tasks, kind: tasks_summary, title: Personal thank-yous, params: {} }
      recipients: [{ type: team, id: "{{ ref.team_report.id }}" }]
      channel_policy: { mode: email_only }

  - ref: wf_thanks
    kind: workflow
    about: Thank-you SMS and email after each recorded gift; no amounts.
    body:
      name: "Donor: thank you"
      description: A thank-you by SMS and email after each recorded gift, at most once a day per donor.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: onboarding.submitted, form_id: "{{ ref.form_record.id }}" }
      steps:
        - { id: s1, type: delay, config: { duration: "{{ params.thanks_delay }}" } }
        - { id: s2, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_thanks_sms.id }}" } } } }
        - { id: s3, 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: 24
      goal: {}
      exit_conditions: []
  - ref: wf_first_gift
    kind: workflow
    about: A first gift gives the thank-you team a task to thank the donor personally; saves the outcome.
    body:
      name: "Donor: first-time donor call"
      description: On someone's first recorded gift, the thank-you team thanks them personally. The outcome is saved on the donor.
      enabled: false
      trigger_type: event
      trigger_config:
        event_name: onboarding.submitted
        form_id: "{{ ref.form_record.id }}"
        property_filters: [{ property: first_gift, cond_operator: eq, value: "yes" }]
        properties: { $origamy_property_filters: true }
      steps:
        - id: s1
          type: human_task
          config:
            human_task:
              title: "First-time donor: {{ traits.first_name }} {{ traits.last_name }}"
              instructions: |-
                {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) gave for the first time.
                Please thank them personally: a call, a voice note or a handwritten card. Don't mention the amount.
                Ask if they would like to hear how their gift is used, and note anything they tell you.
              assignee_refs: [{ type: team, id: "{{ ref.team_directors.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: thanked, label: Thanked, intent: positive }
                - { 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: { first_gift_call: "{{ properties.task_outcome | default: \"unknown\" }}", first_gift_note: "{{ properties.task_note }}" } } }
      re_entry_policy: once
      re_entry_cooldown_hours: 24
      goal: {}
      exit_conditions: []
  - ref: wf_year
    kind: workflow
    about: Daily run over "a year since the last gift"; one impact email and SMS per year.
    body:
      name: "Donor: a year of support"
      description: Every day, an impact email and SMS to donors whose last gift was about a year ago.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_year.id }}", rrule: FREQ=DAILY, starts_at: "{{ workspace.tomorrow }}T{{ params.send_time }}", timezone: "{{ params.timezone }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_year_email.id }}" } } } }
        - { id: s2, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_year_sms.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 7200
      goal: {}
      exit_conditions: []
  - ref: wf_lapsed
    kind: workflow
    about: Daily run over lapsed donors; one gentle email.
    body:
      name: "Donor: lapsed re-engagement"
      description: Every day, one email to donors whose last gift was 18 to 30 months ago.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_lapsed.id }}", rrule: FREQ=DAILY, starts_at: "{{ workspace.tomorrow }}T{{ params.send_time }}", timezone: "{{ params.timezone }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_lapsed_email.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 8760
      goal: {}
      exit_conditions: []

eval:
  name: 91_nonprofits_donor_thank_you
  mode: task
  vertical: ""
  prompt: >-
    We're a small charity. Our team will record each gift on a form. Thank the donor by SMS and
    email the same day without mentioning how much they gave. If it's their first gift, give our
    director a task to thank them personally within 3 days. About a year after someone's last
    gift, email them about what their support has done and invite them to give again. Send our
    fundraising team a monthly count of donors and first-time donors.
  expect_artifacts: [form, segment, workflow, message_template, report]
  forbid_terms: [discount, cart, checkout, purchase, revenue, customers, upsell]
