# Origamy playbook spec v1. Reference: /playbooks/spec-format/
spec_version: 1
id: communities.dues-renewals
title: Membership dues renewals
sector: communities
source:
  kind: synthesized
  notes: >-
    No association or club has built this with us yet. Written from the insurance renewal
    cadence (an email a month ahead, a short reminder a week ahead, a lapsed message and a
    call) and the cooperative contributions pattern (a member register in a synced sheet, a
    friendly tone, a treasurer who calls). Dues are usually yearly, so the register holds the
    date each member's dues run out.
derived_from: lending.cooperative-contributions
vocabulary:
  person: member
  people: members
  group: executive
summary: >-
  Keeps membership dues renewed without the treasurer chasing by hand. Members get a friendly
  email a month before their dues run out and an SMS (or WhatsApp) a week before. If dues have
  lapsed, they get a kind message and the treasurer gets a task to call. The executive gets a
  monthly count of members who are paid, due and lapsed, and you always have a list of members
  in good standing.
when_to_use:
  - You run an association, professional body, alumni network, club, faith group or diaspora group that collects yearly dues.
  - Your treasurer or secretary keeps the member register in Google Sheets or Excel with the date each member's dues run out.
  - Members forget to renew, and the treasurer spends weeks reminding people one by one.
  - You need a clean list of paid-up members before an annual general meeting.

requires:
  data_in:
    - how: sheet_sync
      what: "Your member register, synced every hour as People: phone, email, first name, chapter, the date dues expire and dues status (paid or unpaid)."
    - how: csv_import
      what: "Or a one-off CSV of the same columns to start with."
  channels:
    - channel: email
      for: The first reminder, a month before
      needs: A verified sending domain for {{ params.email_from }}.
    - channel: sms
      for: The week-before reminder and the lapsed message (or WhatsApp, if you switch it on)
      needs: An SMS provider, or Origamy managed SMS after business verification (KYC).
    - channel: whatsapp
      for: The week-before reminder and the lapsed message, if you answer yes to WhatsApp
      needs: A connected WhatsApp Business number, and both templates approved by Meta.
  events: []
  traits:
    - "{{ params.dues_date_trait }}"
    - "{{ params.dues_status_trait }}"
    - chapter
    - dues_call
    - dues_note

parameters:
  - key: org_name
    ask: true
    question: What should messages call your association or group?
    label: Group name
    type: string
    example: Northwind Alumni Network
  - key: how_to_pay
    ask: true
    question: How should members pay their dues? (the sentence members will read, without account numbers)
    help: Say it the way you would say it out loud. Messages never carry bank details or amounts.
    label: How to pay
    type: text
    example: Pay by bank transfer to the association account, or give it to the treasurer at the next meeting.
  - key: treasurer_team
    ask: true
    question: Which team calls members whose dues have lapsed? (usually the treasurer, one person is fine)
    label: Treasurer
    type: team_name
    default: Treasurer
  - key: executive_team
    ask: true
    question: Who gets the monthly report?
    label: Executive
    type: team_name
    default: Executive committee
  - key: use_whatsapp
    ask: true
    question: Should the week-before and lapsed messages go by WhatsApp instead of SMS?
    help: Needs a connected WhatsApp Business number and Meta-approved templates.
    label: Use WhatsApp
    type: boolean
    default: false
  - key: treasurer_phone
    label: Number members can call or message with questions
    type: string
    default: "{{ workspace.phone }}"
  - key: dues_date_trait
    label: Trait with the date dues expire
    type: trait_key
    default: dues_expires_at
  - key: dues_status_trait
    label: Trait that says if dues are paid (paid or unpaid)
    type: trait_key
    default: dues_status
  - key: email_lead
    label: Email this long before dues expire
    type: duration
    default: 30d
  - key: sms_lead
    label: SMS or WhatsApp this long before dues expire
    type: duration
    default: 7d
  - key: lapsed_window
    label: How long after expiry to keep following up lapsed dues
    type: duration
    default: 30d
  - key: call_due_hours
    label: Hours the treasurer has to make the call
    type: hours
    default: 72
  - key: send_time
    label: Daily send time
    type: time
    default: "09:00"
  - key: report_cron
    label: Monthly report (first day of the month)
    type: cron
    default: "0 8 1 * *"
  - key: timezone
    label: Time zone for the daily run and the report
    type: timezone
    default: "{{ workspace.timezone }}"
  - key: email_from
    label: Sender address
    type: email
    default: "{{ workspace.default_sender }}"
  - key: greeting_fallback
    label: Greeting when a first name is missing
    type: string
    default: friend

timings:
  - Runs every day at {{ params.send_time }} in {{ params.timezone }}.
  - An email when dues expire within {{ params.email_lead }}, and an SMS (or WhatsApp) when they expire within {{ params.sms_lead }}. Each only while the member is not marked paid, and each member gets each message once per cycle.
  - Once dues have lapsed (expired more than a day ago, up to {{ params.lapsed_window }}) the member gets a kind message. Two days later, if they are still unpaid, {{ params.treasurer_team }} gets a call task due in {{ params.call_due_hours }} hours.
  - "Monthly report to {{ params.executive_team }}: cron {{ params.report_cron }} in {{ params.timezone }}."

customer_must:
  - "Keep these columns in your member register: phone, email, first_name, chapter, {{ params.dues_date_trait }} (a date like 2026-12-31) and {{ params.dues_status_trait }} (exactly paid or unpaid, never blank). Set the status to paid when dues are received. When the new year starts, move the date forward a year and set the status back to unpaid, and leave the old date in place for a few weeks first if you want the monthly report to count recent renewals."
  - Add your treasurer to the {{ params.treasurer_team }} team and your executive to {{ params.executive_team }} (Settings → Collaborators → Teams). If one person does both, put them in both. A task or report sent to an empty team reaches nobody.
  - "If you chose WhatsApp: connect your WhatsApp Business number and submit the two templates (dues_renewal_reminder and dues_lapsed_notice) for Meta approval before switching the follow-ups on."
  - Verify the sending domain for the email. For SMS, connect a provider or finish business verification.
  - Tell members at a meeting or in the group chat that reminders will come by email, SMS or WhatsApp, and that they can reply STOP. Messages never show amounts, so you stay in control of what is said about money.
  - Keep reminders friendly. Never name members who haven't paid, and never contact someone's family or employer. Make sure the treasurer is ready to hear from members who can't pay this year.
  - Check the segment sizes against your register, then switch the follow-ups and the report on and test with your own phone and a test row.

objects:
  - ref: team_treasurer
    kind: team
    about: Calls members whose dues have lapsed.
    body: { name: "{{ params.treasurer_team }}" }
  - ref: team_executive
    kind: team
    about: Gets the monthly report.
    body: { name: "{{ params.executive_team }}" }

  - ref: seg_email_window
    kind: segment
    about: Dues expire within the email lead time and are not paid.
    body:
      name: Dues expire within a month
      description: "{{ params.dues_date_trait }} within the next {{ params.email_lead }}, not paid."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.dues_date_trait }}", cond_operator: within_next, value: "{{ params.email_lead }}" }
          - { type: trait, trait_key: "{{ params.dues_status_trait }}", cond_operator: neq, value: paid }
  - ref: seg_sms_window
    kind: segment
    about: Dues expire within the SMS lead time and are not paid.
    body:
      name: Dues expire within a week
      description: "{{ params.dues_date_trait }} within the next {{ params.sms_lead }}, not paid."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.dues_date_trait }}", cond_operator: within_next, value: "{{ params.sms_lead }}" }
          - { type: trait, trait_key: "{{ params.dues_status_trait }}", cond_operator: neq, value: paid }
  - ref: seg_lapsed
    kind: segment
    about: Dues expired more than a day ago, within the follow-up window, and not paid.
    body:
      name: Dues lapsed
      description: "{{ params.dues_date_trait }} more than a day ago but within {{ params.lapsed_window }}, not paid."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.dues_date_trait }}", cond_operator: older_than, value: 1d }
          - { type: trait, trait_key: "{{ params.dues_date_trait }}", cond_operator: within_last, value: "{{ params.lapsed_window }}" }
          - { type: trait, trait_key: "{{ params.dues_status_trait }}", cond_operator: neq, value: paid }
  - ref: seg_good_standing
    kind: segment
    about: Members who have paid and whose dues haven't expired yet. Open it for the list of paid-up members before a general meeting.
    body:
      name: Members in good standing
      description: Dues are marked paid and the expiry date is still ahead.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.dues_status_trait }}", cond_operator: eq, value: paid }
          - { type: trait, trait_key: "{{ params.dues_date_trait }}", cond_operator: within_next, value: 800d }

  - ref: tmpl_email
    kind: template
    about: Friendly email a month before dues expire.
    body:
      name: Dues coming up (email)
      channel: email
      category: Dues
      style: html
      status: active
      subject: "Your {{ params.org_name }} membership"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "A friendly note about renewing." } }
      variables: {}
      body: >-
        <p>Hello {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>Thank you for being part of {{ params.org_name }}. Your membership dues run until
        {{ traits.{{ params.dues_date_trait }} | date: "%-d %B %Y" }}, and we would love to have you with us
        for another year.</p>
        <p>{{ params.how_to_pay }}</p>
        <p>If you have already paid, thank you. If this is a hard year, please talk to our
        treasurer on {{ params.treasurer_phone }}. We would rather help than lose you.</p>
        <p>Warm regards,<br>{{ params.org_name }}</p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>
  - ref: tmpl_soon_sms
    kind: template
    about: SMS a week before dues expire.
    body:
      name: Dues in a week (SMS)
      channel: sms
      category: Dues
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: hello {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, a friendly reminder that your membership dues run until {{ traits.{{ params.dues_date_trait }} | date: \"%-d %b\" }}. {{ params.how_to_pay }} Already paid? Thank you. Questions? Call {{ params.treasurer_phone }}. Reply STOP to opt out."
  - ref: tmpl_lapsed_sms
    kind: template
    about: Kind SMS once dues have lapsed. No pressure.
    body:
      name: Dues lapsed (SMS)
      channel: sms
      category: Dues
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: hello {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, your membership dues ran out on {{ traits.{{ params.dues_date_trait }} | date: \"%-d %b\" }}. We would love to keep you with us. {{ params.how_to_pay }} If you have paid, or need a little time, please tell the treasurer on {{ params.treasurer_phone }}. Reply STOP to opt out."
  - ref: tmpl_soon_wa
    kind: template
    when: { param: use_whatsapp, equals: true }
    about: WhatsApp reminder a week before dues expire (needs Meta approval).
    body:
      name: Dues in a week (WhatsApp)
      channel: whatsapp
      category: Dues
      style: text
      status: active
      subject: ""
      provider_template_id: dues_renewal_reminder
      metadata: { whatsapp: { category: UTILITY, language: en } }
      variables:
        "1": "{{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}"
        "2": "{{ params.org_name }}"
        "3": "{{ traits.{{ params.dues_date_trait }} | date: \"%-d %b\" }}"
        "4": "{{ params.how_to_pay }}"
        "5": "{{ params.treasurer_phone }}"
      body: "Hello {{1}}, a friendly reminder from {{2}}: your membership dues run until {{3}}. {{4}} If you have already paid, thank you. Questions? Contact the treasurer on {{5}}."
  - ref: tmpl_lapsed_wa
    kind: template
    when: { param: use_whatsapp, equals: true }
    about: Kind WhatsApp message once dues have lapsed (needs Meta approval).
    body:
      name: Dues lapsed (WhatsApp)
      channel: whatsapp
      category: Dues
      style: text
      status: active
      subject: ""
      provider_template_id: dues_lapsed_notice
      metadata: { whatsapp: { category: UTILITY, language: en } }
      variables:
        "1": "{{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}"
        "2": "{{ params.org_name }}"
        "3": "{{ traits.{{ params.dues_date_trait }} | date: \"%-d %b\" }}"
        "4": "{{ params.how_to_pay }}"
        "5": "{{ params.treasurer_phone }}"
      body: "Hello {{1}}, this is {{2}}. Your membership dues ran out on {{3}}, and we would love to keep you with us. {{4}} If you have paid, or need a little time, please tell the treasurer on {{5}}."

  - ref: report_monthly
    kind: report
    about: Each month, how many members are due, in good standing and lapsed, to the executive.
    body:
      name: "Monthly: dues"
      description: How many members' dues expire soon, how many are in good standing and how many have lapsed, with the state of the treasurer's calls.
      schedule_cron: "{{ params.report_cron }}"
      timezone: "{{ params.timezone }}"
      enabled: false
      spec:
        period: previous_month
        compare: previous_period
        week_start: monday
        sections:
          - id: sizes
            kind: segment_sizes
            title: Dues now
            params: { segment_ids: ["{{ ref.seg_email_window.id }}", "{{ ref.seg_good_standing.id }}", "{{ ref.seg_lapsed.id }}"] }
          - { id: tasks, kind: tasks_summary, title: Treasurer calls, params: {} }
          - { id: note, kind: text, params: { body: "These are the numbers as the register stands today. The list of members in good standing is the segment of that name." } }
      recipients: [{ type: team, id: "{{ ref.team_executive.id }}" }]
      channel_policy: { mode: email_only }

  - ref: wf_email
    kind: workflow
    about: 'Daily run over "dues expire within a month": one email per cycle.'
    body:
      name: "Dues 1: a month before"
      description: Every day, a friendly email to members whose dues expire within a month and aren't paid.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_email_window.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_email.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 1080
      goal: {}
      exit_conditions: []
  - ref: wf_soon
    kind: workflow
    about: 'Daily run over "dues expire within a week": one SMS or WhatsApp per cycle.'
    body:
      name: "Dues 2: a week before"
      description: Every day, a short reminder to members whose dues expire within a week and aren't paid.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_sms_window.id }}", rrule: FREQ=DAILY, starts_at: "{{ workspace.tomorrow }}T{{ params.send_time }}", timezone: "{{ params.timezone }}" }
      steps:
        - { id: s1, type: action, when: { param: use_whatsapp, equals: false }, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_soon_sms.id }}" } } } }
        - { id: s2, type: action, when: { param: use_whatsapp, equals: true }, config: { action_type: send_message, channel_config: { mode: single, single: { channel: whatsapp, template_id: "{{ ref.tmpl_soon_wa.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 480
      goal: {}
      exit_conditions: []
  - ref: wf_lapsed
    kind: workflow
    about: 'Daily run over "dues lapsed": a kind message, then a call task for the treasurer with the outcome saved.'
    body:
      name: "Dues 3: lapsed"
      description: A kind message to members whose dues have lapsed, then a call task for the treasurer if they still haven't paid. The outcome is saved on the member.
      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, when: { param: use_whatsapp, equals: false }, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_lapsed_sms.id }}" } } } }
        - { id: s2, type: action, when: { param: use_whatsapp, equals: true }, config: { action_type: send_message, channel_config: { mode: single, single: { channel: whatsapp, template_id: "{{ ref.tmpl_lapsed_wa.id }}" } } } }
        - { id: s3, type: delay, config: { duration: 2d } }
        - { id: s4, type: filter, config: { condition: { type: trait, trait_key: "{{ params.dues_status_trait }}", cond_operator: neq, value: paid } } }
        - id: s5
          type: human_task
          config:
            human_task:
              title: "Dues check-in: {{ traits.first_name }} {{ traits.last_name }}, {{ traits.chapter | default: \"no chapter\" }}"
              instructions: |-
                {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) has not renewed their membership dues, even after a message.

                Check the register first: if they have paid, update the sheet and close this task. Otherwise call the member only, kindly. Ask if all is well, and offer a way forward if this is a hard year. Never speak to their family, employer or other members about it. Choose an outcome and add a short note.
              assignee_refs: [{ type: team, id: "{{ ref.team_treasurer.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: will_pay, label: Will renew, intent: positive }
                - { id: paid, label: Already paid, intent: positive }
                - { id: needs_time, label: Needs more time or help, intent: neutral }
                - { id: leaving, label: Not renewing, intent: neutral }
                - { id: not_reached, label: Could not reach, intent: negative }
              due_hours: "{{ params.call_due_hours }}"
              wait: true
              wait_timeout_hours: 240
              notify: immediate
        - { id: s6, type: action, config: { action_type: set_trait, traits: { dues_call: "{{ properties.task_outcome | default: \"unknown\" }}", dues_note: "{{ properties.task_note }}" } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 840
      goal: {}
      exit_conditions: []

eval:
  name: 112_communities_dues_renewals
  mode: task
  vertical: ""
  prompt: >-
    We're an association that collects yearly dues. Our members sheet has phone, email, chapter,
    dues_expires_at and dues_status (paid or unpaid). Email members a month before their dues
    expire and text them a week before. If dues have lapsed, text them kindly and give our
    treasurer a task to call. Send our executive a monthly report of how many members are
    paid, due and lapsed. Keep the tone friendly and don't put amounts in messages.
  expect_artifacts: [segment, workflow, message_template, team, report]
  forbid_terms: [penalty, suspended, late fee]
