# Origamy playbook spec v1. Reference: /playbooks/spec-format/
spec_version: 1
id: insurance.policy-renewals
title: Policy renewal reminders
sector: insurance
source:
  kind: synthesized
  notes: >-
    No broker or agent has built this with us yet. Written from the base payment-reminder
    pattern (a date trait, segments over it, a daily schedule) and the call-task escalation of
    the first-contact follow-up, with a monthly report to the principal. Messages never carry
    premiums or policy numbers.
derived_from: base.payment-reminder
vocabulary:
  person: client
  people: clients
  group: renewals desk
summary: >-
  Reminds clients to renew their policy by email a month before, by SMS two weeks before, and
  gives your renewals desk a call task a week before if they still haven't renewed. Anyone who
  lets a policy lapse gets a kind SMS and a call. The principal gets a monthly summary.
when_to_use:
  - You are an insurance agent or broker with a client list in Google Sheets or Excel, each with a renewal date.
  - Clients forget to renew, and you chase them by phone one at a time.
  - You want to see each month how many renewals are due, renewed and lapsed.

requires:
  data_in:
    - how: sheet_sync
      what: "Your policy sheet, synced every hour as People: phone, email, policy type, renewal date and renewal status (pending, renewed or lapsed)."
    - 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 two-week reminder and the lapsed message
      needs: An SMS provider, or Origamy managed SMS after business verification (KYC).
  events: []
  traits:
    - "{{ params.renewal_date_trait }}"
    - "{{ params.renewal_status_trait }}"
    - policy_type
    - renewal_call
    - renewal_note
    - lapsed_call

parameters:
  - key: org_name
    ask: true
    question: What should messages call your agency or brokerage?
    label: Business name
    type: string
    example: Northwind Insurance Brokers
  - key: contact_phone
    ask: true
    question: Which number should clients call to renew or ask questions?
    label: Renewals phone
    type: string
    default: "{{ workspace.phone }}"
  - key: renewals_team
    ask: true
    question: Which team calls clients who haven't renewed? (one person is fine)
    label: Renewals desk
    type: team_name
    default: Renewals desk
  - key: principal_team
    ask: true
    question: Who should hear if a renewal call isn't made, and get the monthly summary?
    label: Principal
    type: team_name
    default: Principals
  - key: renew_url
    ask: true
    question: Is there a link where clients can renew or request a quote? (leave empty to give your phone number only)
    label: Renewal link
    type: url
    default: ""
  - key: renewal_date_trait
    label: Trait with the renewal date
    type: trait_key
    default: renewal_date
  - key: renewal_status_trait
    label: Trait with the renewal status (pending, renewed or lapsed)
    type: trait_key
    default: renewal_status
  - key: email_lead
    label: Email this long before the renewal date
    type: duration
    default: 30d
  - key: sms_lead
    label: SMS this long before the renewal date
    type: duration
    default: 14d
  - key: call_lead
    label: Call task this long before the renewal date
    type: duration
    default: 7d
  - key: lapsed_window
    label: How long after the renewal date to keep following up a lapsed policy
    type: duration
    default: 14d
  - key: call_due_hours
    label: Hours the desk has to make the call
    type: hours
    default: 48
  - key: escalate_after_hours
    label: Hours after which an uncalled task goes to the principal
    type: hours
    default: 48
  - key: send_time
    label: Daily send time
    type: time
    default: "09:00"
  - key: report_cron
    label: When the monthly summary arrives
    type: cron
    default: "0 8 1 * *"
  - key: timezone
    label: Time zone for the daily runs 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: there

timings:
  - Runs every day at {{ params.send_time }} in {{ params.timezone }}.
  - An email when the renewal date is within {{ params.email_lead }}, an SMS within {{ params.sms_lead }}, and a call task for {{ params.renewals_team }} within {{ params.call_lead }} (due in {{ params.call_due_hours }} hours, escalating to {{ params.principal_team }} after {{ params.escalate_after_hours }} hours). Each only while the policy is still pending.
  - A lapsed policy (renewal date passed in the last {{ params.lapsed_window }}, not renewed) gets an SMS and a call task. Each client gets each message once per cycle.
  - The monthly summary arrives on cron {{ params.report_cron }}.

customer_must:
  - "Keep these columns in your policy sheet: phone, email, policy_type (for example motor or home, in plain words), {{ params.renewal_date_trait }} (a date like 2026-12-01) and {{ params.renewal_status_trait }} (exactly pending, renewed or lapsed, never blank). Set the status to renewed as soon as a client pays. Leave the old date in place for a few weeks, so the monthly summary can count the renewal, then move it forward a year and set the status back to pending."
  - Add people to the {{ params.renewals_team }} team and to {{ params.principal_team }}. If you work alone, put yourself in both. A task assigned to an empty team reaches nobody.
  - Verify the sending domain for the email. For SMS, connect a provider or finish business verification.
  - Check the segment sizes against your sheet, then switch the follow-ups and the report on.
  - Never put premiums, policy numbers or claim details in messages or in the notes on a task. Follow your insurance regulator's rules on how and when you may contact clients, and do not tell clients they are covered unless they are.

objects:
  - ref: team_renewals
    kind: team
    about: Calls clients who haven't renewed.
    body: { name: "{{ params.renewals_team }}" }
  - ref: team_principal
    kind: team
    about: Hears about renewal calls that weren't made, and gets the monthly summary.
    body: { name: "{{ params.principal_team }}" }

  - ref: seg_email_window
    kind: segment
    about: Renewal date within the email lead time and still pending.
    body:
      name: Renewal due in a month
      description: "{{ params.renewal_date_trait }} within the next {{ params.email_lead }}, still pending."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.renewal_date_trait }}", cond_operator: within_next, value: "{{ params.email_lead }}" }
          - { type: trait, trait_key: "{{ params.renewal_status_trait }}", cond_operator: eq, value: pending }
  - ref: seg_sms_window
    kind: segment
    about: Renewal date within the SMS lead time and still pending.
    body:
      name: Renewal due in two weeks
      description: "{{ params.renewal_date_trait }} within the next {{ params.sms_lead }}, still pending."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.renewal_date_trait }}", cond_operator: within_next, value: "{{ params.sms_lead }}" }
          - { type: trait, trait_key: "{{ params.renewal_status_trait }}", cond_operator: eq, value: pending }
  - ref: seg_call_window
    kind: segment
    about: Renewal date within the call lead time and still pending.
    body:
      name: Renewal due in a week
      description: "{{ params.renewal_date_trait }} within the next {{ params.call_lead }}, still pending."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.renewal_date_trait }}", cond_operator: within_next, value: "{{ params.call_lead }}" }
          - { type: trait, trait_key: "{{ params.renewal_status_trait }}", cond_operator: eq, value: pending }
  - ref: seg_lapsed
    kind: segment
    about: Renewal date passed in the last couple of weeks and not renewed.
    body:
      name: Recently lapsed
      description: "{{ params.renewal_date_trait }} within the last {{ params.lapsed_window }}, not renewed."
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.renewal_date_trait }}", cond_operator: within_last, value: "{{ params.lapsed_window }}" }
          - { type: trait, trait_key: "{{ params.renewal_status_trait }}", cond_operator: neq, value: renewed }
  - ref: seg_renewed
    kind: segment
    about: Clients who renewed in the last month (for the monthly summary).
    body:
      name: Renewed recently
      description: Status is renewed and the renewal date was within the last 30 days.
      segment_type: dynamic
      status: active
      definition:
        operator: and
        conditions:
          - { type: trait, trait_key: "{{ params.renewal_date_trait }}", cond_operator: within_last, value: 30d }
          - { type: trait, trait_key: "{{ params.renewal_status_trait }}", cond_operator: eq, value: renewed }

  - ref: report_monthly
    kind: report
    about: Each month, how many renewals are coming up, how many renewed and how many lapsed, to the principal.
    body:
      name: "Monthly: renewals"
      description: Renewals due in the next month, renewed in the last month and recently lapsed, with the state of the call tasks.
      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_email_window.id }}", "{{ ref.seg_renewed.id }}", "{{ ref.seg_lapsed.id }}"]
          - { id: tasks, kind: tasks_summary, title: Renewal calls, params: {} }
      recipients: [{ type: team, id: "{{ ref.team_principal.id }}" }]
      channel_policy: { mode: email_only }

  - ref: tmpl_email
    kind: template
    about: Email a month before the renewal date.
    body:
      name: Renewal coming up (email)
      channel: email
      category: Renewals
      style: html
      status: active
      subject: "Your {{ traits.policy_type | default: \"insurance\" }} renewal with {{ params.org_name }}"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "A quick note about your renewal." } }
      variables: {}
      body: >-
        <p>Hello {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>Your {{ traits.policy_type | default: "insurance" }} policy with {{ params.org_name }} is due for
        renewal on {{ traits.{{ params.renewal_date_trait }} | date: "%-d %B %Y" }}.</p>
        <p>Renewing in good time means there is no gap in your cover. If anything has changed
        for you, tell us and we will update it before you renew.</p>
        {% if "{{ params.renew_url }}" != "" %}<p><a href="{{ params.renew_url }}">Renew or ask for a quote</a></p>{% endif %}
        <p>You can also call us on {{ params.contact_phone }}.</p>
        <p>Thank you,<br>{{ params.org_name }}</p>
        <p style="font-size:12px;color:#6b6b80"><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>
  - ref: tmpl_sms
    kind: template
    about: SMS two weeks before.
    body:
      name: Renewal in two weeks (SMS)
      channel: sms
      category: Renewals
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "Hello {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, a reminder from {{ params.org_name }}: your {{ traits.policy_type | default: \"insurance\" }} policy is due for renewal on {{ traits.{{ params.renewal_date_trait }} | date: \"%-d %b\" }}.{% if \"{{ params.renew_url }}\" != \"\" %} Renew here: {{ params.renew_url }}{% endif %} Or call {{ params.contact_phone }}. Reply STOP to opt out."
  - ref: tmpl_lapsed_sms
    kind: template
    about: Kind SMS after a lapse. No pressure.
    body:
      name: Policy lapsed (SMS)
      channel: sms
      category: Renewals
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "Hello {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, {{ params.org_name }} here. Your {{ traits.policy_type | default: \"insurance\" }} policy passed its renewal date on {{ traits.{{ params.renewal_date_trait }} | date: \"%-d %b\" }}. We can help you renew or answer any questions. Call {{ params.contact_phone }}. Reply STOP to opt out."

  - ref: wf_email
    kind: workflow
    about: 'Daily run over "renewal due in a month": one email per cycle.'
    body:
      name: "Renewal 1: a month before"
      description: Every day, an email to clients whose policy renews within a month and is still pending.
      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_sms
    kind: workflow
    about: 'Daily run over "renewal due in two weeks": one SMS per cycle.'
    body:
      name: "Renewal 2: two weeks before"
      description: Every day, an SMS to clients whose policy renews within two weeks and is still pending.
      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, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_sms.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 720
      goal: {}
      exit_conditions: []
  - ref: wf_call
    kind: workflow
    about: 'Daily run over "renewal due in a week": a call task that escalates to the principal; the outcome is saved.'
    body:
      name: "Renewal 3: call a week before"
      description: When a policy renews within a week and is still pending, a call task for the renewals desk.
      enabled: false
      trigger_type: schedule
      trigger_config: { segment_id: "{{ ref.seg_call_window.id }}", rrule: FREQ=DAILY, starts_at: "{{ workspace.tomorrow }}T{{ params.send_time }}", timezone: "{{ params.timezone }}" }
      steps:
        - id: s1
          type: human_task
          config:
            human_task:
              title: "Renewal call: {{ traits.first_name }} {{ traits.last_name }}"
              instructions: |-
                {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) has a {{ traits.policy_type | default: "policy" }} due for renewal soon and hasn't renewed.
                Call to check they got our messages, answer questions and help them renew. Don't quote premiums in a message. Note what they said.
              assignee_refs: [{ type: team, id: "{{ ref.team_renewals.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: renewing, label: Will renew, intent: positive }
                - { id: wants_changes, label: Wants changes or a new quote, intent: neutral }
                - { id: not_renewing, 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: 168
              notify: immediate
              escalation_ladder:
                - { after_hours: "{{ params.escalate_after_hours }}", to: [{ type: team, id: "{{ ref.team_principal.id }}" }] }
        - { id: s2, type: action, config: { action_type: set_trait, traits: { renewal_call: "{{ properties.task_outcome | default: \"unknown\" }}", renewal_note: "{{ properties.task_note }}" } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 720
      goal: {}
      exit_conditions: []
  - ref: wf_lapsed
    kind: workflow
    about: 'Daily run over "recently lapsed": an SMS and a call task.'
    body:
      name: "Renewal 4: lapsed"
      description: When a policy has passed its renewal date without renewing, a kind SMS and a call task.
      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: sms, template_id: "{{ ref.tmpl_lapsed_sms.id }}" } } } }
        - id: s2
          type: human_task
          config:
            human_task:
              title: "Lapsed policy: {{ traits.first_name }} {{ traits.last_name }}"
              instructions: |-
                {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) let a {{ traits.policy_type | default: "policy" }} pass its renewal date.
                Call kindly, find out why, and offer to renew or quote again. Note what they said.
              assignee_refs: [{ type: team, id: "{{ ref.team_renewals.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: renewing, label: Will renew, intent: positive }
                - { id: not_renewing, 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: 168
              notify: immediate
        - { id: s3, type: action, config: { action_type: set_trait, traits: { lapsed_call: "{{ properties.task_outcome | default: \"unknown\" }}" } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 720
      goal: {}
      exit_conditions: []

eval:
  name: 87_insurance_policy_renewals
  mode: task
  vertical: ""
  prompt: >-
    I'm an insurance broker. Email clients a month before their policy renewal date, text them
    two weeks before, and a week before give my renewals team a task to call anyone who hasn't
    renewed, escalating to the principal if it isn't done in two days. If a policy lapses, text
    them kindly and give the team a task to call. Never put the premium or policy number in a
    message. The date is in renewal_date and renewal_status says pending, renewed or lapsed.
  expect_artifacts: [segment, workflow, message_template]
  forbid_terms: [premium, policy number]
