# Origamy playbook spec v1. Reference: /playbooks/spec-format/
spec_version: 1
id: government.renewal-deadline-reminders
title: Renewal deadline reminders
sector: government
source:
  kind: synthesized
  notes: >-
    No local government, agency or licensing office has built this with us yet. Written from the
    insurance policy renewal cadence (a date trait in a synced register, segments over it, a
    daily schedule, a monthly report) with the reminders set at 30 days, 7 days, the expiry day
    and 14 days after. The wording is plain and official: it says what expires, when, and how and
    where to renew, and points to the official notice instead of mentioning fines or penalties.
derived_from: insurance.policy-renewals
vocabulary:
  person: licence holder
  people: licence holders
  group: licensing office
summary: >-
  Reminds licence, permit and registration holders before their renewal deadline, on the day it
  falls and again two weeks after, with how and where to renew. Holders marked as renewed get
  nothing. The licensing office gets a monthly count of renewals due next month, renewed and
  lapsed. Messages are short, plain and never mention fines.
when_to_use:
  - You issue licences, permits, registrations, vehicle papers or business premises permits that must be renewed by a date, and many holders renew late or not at all.
  - Your register is in Google Sheets or Excel with each holder's phone, expiry date and whether they have renewed.
  - You want holders reminded in plain words, with the office address and a link, without anyone chasing them one by one.

requires:
  data_in:
    - how: sheet_sync
      what: "Your licence register, synced every hour as People: phone, email, first name, licence type, the expiry date and the renewal status (pending or renewed)."
    - how: csv_import
      what: "Or a one-off CSV of the same columns to start with."
  channels:
    - channel: sms
      for: The reminders at 30 days, 7 days, on the expiry day and after it
      needs: An SMS provider, or Origamy managed SMS after business verification (KYC). Use your office's registered sender name where the country allows it.
    - channel: email
      for: The 30-day reminder and the monthly report
      needs: A verified sending domain for {{ params.email_from }}.
  events: []
  traits:
    - "{{ params.expiry_trait }}"
    - "{{ params.status_trait }}"
    - licence_type

parameters:
  - key: org_name
    ask: true
    question: What should messages call your office or agency?
    label: Office name
    type: string
    example: Northwind City Licensing Office
  - key: renew_place
    ask: true
    question: Where and when can people renew in person? (a few words)
    label: Where to renew
    type: text
    example: the Licensing Office front desk, Monday to Friday, 8am to 4pm
  - key: renew_url
    ask: true
    question: Is there a link to renew online? (leave empty if there is not)
    label: Renewal link
    type: url
    default: ""
  - key: notice_url
    ask: true
    question: Where is your official page or notice about renewal, requirements and fees? (a link)
    help: Messages point here for any detail about fees or rules. They never carry fees or penalties themselves.
    label: Official notice link
    type: url
    example: "https://www.example.com/renewals"
  - key: office_team
    ask: true
    question: Who gets the monthly report? (for example the licensing office head)
    label: Licensing office
    type: team_name
    default: Licensing office
  - key: booking_url
    label: Link to book an office visit (leave empty to skip it)
    type: url
    default: ""
  - key: office_phone
    label: Number holders can call with questions
    type: string
    default: "{{ workspace.phone }}"
  - key: expiry_trait
    label: Trait with the expiry date
    type: trait_key
    default: expires_at
  - key: status_trait
    label: Trait with the renewal status (pending or renewed)
    type: trait_key
    default: renewal_status
  - key: first_lead
    label: First reminder (email and SMS) this long before expiry
    type: duration
    default: 30d
  - key: second_lead
    label: Second reminder (SMS) this long before expiry
    type: duration
    default: 7d
  - key: late_after
    label: Reminder after expiry, this long after the date
    type: duration
    default: 14d
  - key: late_window
    label: Window for the after-expiry reminder (one day longer than the delay, so each holder matches on one morning)
    type: duration
    default: 15d
  - 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: holder

timings:
  - Runs every day at {{ params.send_time }} in {{ params.timezone }}.
  - An email and SMS when the expiry date is within {{ params.first_lead }}, an SMS when it is within {{ params.second_lead }}, an SMS on the expiry day, and an SMS {{ params.late_after }} after the date. Each only while the holder is not marked renewed, and each holder gets each message once per cycle.
  - "Monthly report to {{ params.office_team }}: cron {{ params.report_cron }} in {{ params.timezone }}."

customer_must:
  - "Keep these columns in your licence register: phone, email, first_name, licence_type (in plain words, for example business permit), {{ params.expiry_trait }} (a date like 2026-12-31) and {{ params.status_trait }} (exactly pending or renewed, never blank). Set the status to renewed as soon as a renewal is processed. Leave the old date in place for a few weeks so the monthly report can count it, then move it forward and set the status back to pending."
  - Add the people who should get the monthly report to the {{ params.office_team }} team (Settings → Collaborators → Teams). A report sent to an empty team reaches nobody.
  - Make sure the notice page is public and says what holders need and what renewal costs. The messages never state fees or penalties, so the page has to.
  - Verify the sending domain for the email, and connect an SMS provider or finish business verification. Register your office's sender name with your SMS provider where that is possible.
  - Tell holders when they get or renew a licence that reminders will come by SMS and email, and that they can reply STOP. Only message numbers that holders gave you for this purpose.
  - Keep the tone plain and official. Messages never mention fines, penalties or consequences, and never carry any political or campaign content. Check the wording with your communications office.
  - 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_office
    kind: team
    about: Gets the monthly report.
    body: { name: "{{ params.office_team }}" }

  - ref: seg_first
    kind: segment
    about: Expiry within the first reminder window and not renewed.
    body:
      name: Expires within a month
      description: "{{ params.expiry_trait }} within the next {{ params.first_lead }}, not renewed."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.expiry_trait }}", cond_operator: within_next, value: "{{ params.first_lead }}" }
          - { type: trait, trait_key: "{{ params.status_trait }}", cond_operator: neq, value: renewed }
  - ref: seg_second
    kind: segment
    about: Expiry within the second reminder window and not renewed.
    body:
      name: Expires within a week
      description: "{{ params.expiry_trait }} within the next {{ params.second_lead }}, not renewed."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.expiry_trait }}", cond_operator: within_next, value: "{{ params.second_lead }}" }
          - { type: trait, trait_key: "{{ params.status_trait }}", cond_operator: neq, value: renewed }
  - ref: seg_today
    kind: segment
    about: Expiry today (the date started in the last day) and not renewed.
    body:
      name: Expires today
      description: "{{ params.expiry_trait }} is today, not renewed."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.expiry_trait }}", cond_operator: within_last, value: 1d }
          - { type: trait, trait_key: "{{ params.status_trait }}", cond_operator: neq, value: renewed }
  - ref: seg_late
    kind: segment
    about: Expired about two weeks ago (a one-day band) and not renewed.
    body:
      name: Expired two weeks ago
      description: "{{ params.expiry_trait }} more than {{ params.late_after }} ago but within {{ params.late_window }}, not renewed."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.expiry_trait }}", cond_operator: older_than, value: "{{ params.late_after }}" }
          - { type: trait, trait_key: "{{ params.expiry_trait }}", cond_operator: within_last, value: "{{ params.late_window }}" }
          - { type: trait, trait_key: "{{ params.status_trait }}", cond_operator: neq, value: renewed }
  - ref: seg_renewed
    kind: segment
    about: Holders who renewed in the last month (for the monthly report).
    body:
      name: Renewed recently
      description: Status is renewed and the expiry date was within the last 30 days.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.expiry_trait }}", cond_operator: within_last, value: 30d }
          - { type: trait, trait_key: "{{ params.status_trait }}", cond_operator: eq, value: renewed }
  - ref: seg_lapsed
    kind: segment
    about: Expired in the last 90 days and not renewed (for the monthly report).
    body:
      name: Lapsed
      description: "{{ params.expiry_trait }} more than a day ago but within 90 days, not renewed."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.expiry_trait }}", cond_operator: older_than, value: 1d }
          - { type: trait, trait_key: "{{ params.expiry_trait }}", cond_operator: within_last, value: 90d }
          - { type: trait, trait_key: "{{ params.status_trait }}", cond_operator: neq, value: renewed }

  - ref: tmpl_first_email
    kind: template
    about: Email a month before expiry, with how and where to renew.
    body:
      name: Renewal coming up (email)
      channel: email
      category: Renewals
      style: html
      status: active
      subject: "Your {{ traits.licence_type | default: \"licence\" }} expires on {{ traits.{{ params.expiry_trait }} | date: \"%-d %B %Y\" }}"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "How and where to renew." } }
      variables: {}
      body: >-
        <p>Dear {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>This is a reminder from {{ params.org_name }}. Your {{ traits.licence_type | default: "licence" }}
        expires on {{ traits.{{ params.expiry_trait }} | date: "%-d %B %Y" }}.</p>
        <p><strong>How to renew</strong></p>
        <ul>
        {% if "{{ params.renew_url }}" != "" %}<li>Online: <a href="{{ params.renew_url }}">{{ params.renew_url }}</a></li>{% endif %}
        <li>In person: {{ params.renew_place }}</li>
        {% if "{{ params.booking_url }}" != "" %}<li>To book a visit: <a href="{{ params.booking_url }}">{{ params.booking_url }}</a></li>{% endif %}
        </ul>
        <p>Requirements and fees are in our official notice: <a href="{{ params.notice_url }}">{{ params.notice_url }}</a></p>
        <p>If you have already renewed, thank you. Questions? Call {{ params.office_phone }}.</p>
        <p>Yours sincerely,<br>{{ params.org_name }}</p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>
  - ref: tmpl_first_sms
    kind: template
    about: SMS a month before expiry.
    body:
      name: Renewal in a month (SMS)
      channel: sms
      category: Renewals
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: hello {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, your {{ traits.licence_type | default: \"licence\" }} expires on {{ traits.{{ params.expiry_trait }} | date: \"%-d %b %Y\" }}.{% if \"{{ params.renew_url }}\" != \"\" %} Renew online: {{ params.renew_url }}{% endif %} Or in person: {{ params.renew_place }}. Details: {{ params.notice_url }} Already renewed? Thank you. Reply STOP to opt out."
  - ref: tmpl_second_sms
    kind: template
    about: SMS a week before expiry.
    body:
      name: Renewal in a week (SMS)
      channel: sms
      category: Renewals
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: a reminder, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}. Your {{ traits.licence_type | default: \"licence\" }} expires on {{ traits.{{ params.expiry_trait }} | date: \"%-d %b\" }}.{% if \"{{ params.renew_url }}\" != \"\" %} Renew online: {{ params.renew_url }}{% endif %} Or in person: {{ params.renew_place }}.{% if \"{{ params.booking_url }}\" != \"\" %} Book a visit: {{ params.booking_url }}{% endif %} Questions? Call {{ params.office_phone }}. Reply STOP to opt out."
  - ref: tmpl_today_sms
    kind: template
    about: SMS on the expiry day.
    body:
      name: Renewal today (SMS)
      channel: sms
      category: Renewals
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: hello {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, your {{ traits.licence_type | default: \"licence\" }} expires today.{% if \"{{ params.renew_url }}\" != \"\" %} Renew online: {{ params.renew_url }}{% endif %} Or in person: {{ params.renew_place }}. Questions? Call {{ params.office_phone }}. Reply STOP to opt out."
  - ref: tmpl_late_sms
    kind: template
    about: SMS two weeks after expiry. Plain, with a pointer to the official notice and no mention of penalties.
    body:
      name: Renewal overdue (SMS)
      channel: sms
      category: Renewals
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: hello {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, our records show your {{ traits.licence_type | default: \"licence\" }} expired on {{ traits.{{ params.expiry_trait }} | date: \"%-d %b %Y\" }}.{% if \"{{ params.renew_url }}\" != \"\" %} Renew: {{ params.renew_url }}{% endif %} Or at {{ params.renew_place }}. Details: {{ params.notice_url }} Already renewed? Ignore this. Reply STOP to opt out."

  - ref: report_monthly
    kind: report
    about: Each month, how many renewals are due next month, how many renewed and how many lapsed, to the licensing office.
    body:
      name: "Monthly: renewals"
      description: Licences expiring in the next month, renewed in the last month and lapsed in the last 90 days. Counts only, no names.
      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: Renewals now
            params: { segment_ids: ["{{ ref.seg_first.id }}", "{{ ref.seg_renewed.id }}", "{{ ref.seg_lapsed.id }}"] }
          - { id: note, kind: text, params: { body: "These are the numbers as the register stands today. Due next month counts holders not yet marked renewed, renewed counts holders marked renewed whose date was in the last 30 days, and lapsed counts holders not renewed in the last 90 days." } }
      recipients: [{ type: team, id: "{{ ref.team_office.id }}" }]
      channel_policy: { mode: email_only }

  - ref: wf_first
    kind: workflow
    about: 'Daily run over "expires within a month": one email and SMS per cycle.'
    body:
      name: "Renewal 1: a month before"
      description: Every day, an email and SMS to holders whose licence expires within a month and isn't renewed.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_first.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_first_email.id }}" } } } }
        - { id: s2, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_first_sms.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 1080
      goal: {}
      exit_conditions: []
  - ref: wf_second
    kind: workflow
    about: 'Daily run over "expires within a week": one SMS per cycle.'
    body:
      name: "Renewal 2: a week before"
      description: Every day, an SMS to holders whose licence expires within a week and isn't renewed.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_second.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: sms, template_id: "{{ ref.tmpl_second_sms.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 480
      goal: {}
      exit_conditions: []
  - ref: wf_today
    kind: workflow
    about: 'Daily run over "expires today": one SMS.'
    body:
      name: "Renewal 3: expiry day"
      description: Every day, an SMS to holders whose licence expires today and isn't renewed.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_today.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: sms, template_id: "{{ ref.tmpl_today_sms.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 72
      goal: {}
      exit_conditions: []
  - ref: wf_late
    kind: workflow
    about: 'Daily run over "expired two weeks ago": one SMS.'
    body:
      name: "Renewal 4: two weeks after"
      description: Every day, an SMS to holders whose licence expired about two weeks ago and isn't renewed.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_late.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: sms, template_id: "{{ ref.tmpl_late_sms.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 720
      goal: {}
      exit_conditions: []

eval:
  name: 116_government_renewal_deadline_reminders
  mode: task
  vertical: ""
  prompt: >-
    We're a licensing office. Our register has each holder's phone, email, licence type,
    expires_at and renewal_status (pending or renewed). Email and text holders 30 days before
    their licence expires, text them 7 days before, on the day, and 14 days after, with how and
    where to renew and a link to our official notice. Don't mention fines or penalties. Send our
    office head a monthly report of renewals due next month, renewed and lapsed.
  expect_artifacts: [segment, workflow, message_template, team, report]
  forbid_terms: [penalty, prosecution, fined, arrest]
