# Origamy playbook spec v1. Reference: /playbooks/spec-format/
spec_version: 1
id: insurtech.quote-to-policy
title: Quote to policy
sector: insurtech
source:
  kind: synthesized
  notes: >-
    No insurer has built this with us yet. Written from the base first-contact call task, the
    trial-to-paid nudge sequence (messages that stop when the person buys) and the staff-form
    pattern for a sale recorded by hand. It works from a quote form and a staff "policy sold"
    form with no code, or from quote and purchase events sent by your app. No message carries
    a premium, an amount or a policy number.
derived_from: saas.trial-to-paid
vocabulary:
  person: customer
  people: customers
  group: advisors
summary: >-
  Follows up every quote until it becomes a policy. A reminder email goes out two hours after
  the quote, an SMS the next day and, if the customer still hasn't bought after two days, an
  advisor gets a task to call. It all stops when they buy, and a welcome message explains how
  to claim and where to find the policy document. The manager gets a weekly count of quotes and
  purchases.
when_to_use:
  - You are a digital insurer, an MGA or a broker who quotes online, and most quotes never turn into policies.
  - You want an advisor to call only the people who are still thinking, not everyone.
  - You want new policyholders to know how to claim from day one.

requires:
  data_in:
    - how: public_form
      what: "The Get a quote form this playbook creates (when you choose the form option): put its link on your website, WhatsApp and social bios. No code needed."
    - how: staff_form
      what: "The Policy sold form your advisors fill in when a customer buys, matched by phone. It stops the follow-up and sends the welcome."
    - how: app_events
      what: "Or, with the app option, your app sends a quote event (with the product) and a purchase event."
  channels:
    - channel: email
      for: The quote reminder and the welcome
      needs: A verified sending domain for {{ params.email_from }}.
    - channel: sms
      for: The next-day reminder and the short welcome
      needs: An SMS provider, or Origamy managed SMS after business verification (KYC).
  events:
    - onboarding.submitted
    - "{{ params.quote_event }}"
    - "{{ params.purchased_event }}"
  traits:
    - policy_status
    - quote_product
    - quote_call
    - quote_note
    - policy_sold_at

parameters:
  - key: org_name
    ask: true
    question: What should messages call your company?
    label: Company name
    type: string
    example: Northwind Insurance
  - key: quote_source
    ask: true
    question: "How do quotes and sales reach Origamy? Choose form (no code: a quote form and a staff sold form) or app (your app sends events)."
    label: Data source
    type: enum
    options: [form, app]
    default: form
  - key: advisor_team
    ask: true
    question: Which team calls customers who haven't bought after two days? (one person is fine)
    label: Advisors
    type: team_name
    default: Advisors
  - key: claims_phone
    ask: true
    question: Which number should customers call to make a claim or ask a question?
    label: Claims phone
    type: string
    default: "{{ workspace.phone }}"
  - key: finish_url
    ask: true
    question: Where can customers finish buying their quote? (a link; leave empty to give your phone number only)
    label: Buy link
    type: url
    default: ""
  - key: quote_event
    label: Event your app sends when a quote is created (with a product property)
    type: event_name
    default: quote.created
  - key: purchased_event
    label: Event your app sends when a policy is bought
    type: event_name
    default: policy.purchased
  - key: products
    label: Products on the quote form
    type: list
    default: [Motor, Home, Travel, Business]
  - key: docs_where
    label: Where customers find their policy document
    type: text
    default: in your account in the app and in the email we sent you
  - key: manager_team
    label: Who gets the weekly report
    type: team_name
    default: Sales managers
  - key: email_delay
    label: How long after the quote the reminder email goes
    type: duration
    default: 2h
  - key: sms_delay
    label: Wait after the email before the SMS (a day after the quote, minus the email delay)
    type: duration
    default: 22h
  - key: call_delay
    label: Wait after the SMS before the advisor task
    type: duration
    default: 1d
  - key: call_due_hours
    label: Hours the advisor has to call
    type: hours
    default: 24
  - key: report_cron
    label: When the weekly report arrives
    type: cron
    default: "0 8 * * 1"
  - key: default_country
    label: Default phone country on the forms
    type: country_code
    default: "{{ workspace.country }}"
  - key: timezone
    label: Time zone for 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:
  - "{{ params.email_delay }} after the quote, a reminder email."
  - A day after the quote, an SMS.
  - Two days after the quote, a call task for {{ params.advisor_team }}, due in {{ params.call_due_hours }} hours.
  - Every step checks that the customer hasn't bought yet. The moment a policy is recorded as sold, the reminders stop and the welcome email and SMS go.
  - The weekly report arrives on cron {{ params.report_cron }} in {{ params.timezone }}.

customer_must:
  - "With the form option: publish the Get a quote form and the Policy sold form. Advisors fill in Policy sold, with the phone number from the quote, as soon as a customer buys. With the app option: send {{ params.quote_event }} (with a product property such as motor or travel) and {{ params.purchased_event }} from your app."
  - Add people to the {{ params.advisor_team }} and {{ params.manager_team }} teams. A task assigned to an empty team reaches nobody.
  - "Never put a premium, an amount, a policy number or a claim detail in a message, an event property you copy into a message or a task note. Messages point to the app, the email and your phone number instead."
  - Check your insurance regulator's rules on how you may contact people who asked for a quote, and say on the quote form that you will follow up. Don't tell anyone they are covered until the policy is issued.
  - Verify the sending domain for the email, connect an SMS provider or finish business verification, switch the follow-ups and the report on, and test with your own phone and a fake quote.

objects:
  - ref: team_advisors
    kind: team
    about: Calls customers who haven't bought two days after their quote.
    body: { name: "{{ params.advisor_team }}" }
  - ref: team_managers
    kind: team
    about: Gets the weekly report.
    body: { name: "{{ params.manager_team }}" }

  - ref: form_quote
    kind: form
    about: The quote request form (the no-code way in). The product is saved with the request.
    when: { param: quote_source, equals: form }
    body:
      name: Get a quote
      slug: get-a-quote
      description: Tell us what you'd like to insure and we'll follow up with your quote.
      draft_state:
        version: 1
        meta: { layout: classic, submit_label: Get my quote, success_message: "Thank you! We'll be in touch with your quote." }
        pages:
          - id: p1
            title: Get a quote
            description: A few details and we'll follow up.
            fields:
              - { id: f_first, key: first_name, type: short_text, label: First name, required: true, maps_to: { mode: custom, key: first_name, trait: true } }
              - { id: f_last, key: last_name, type: short_text, label: Surname, required: true, maps_to: { mode: custom, key: last_name, trait: true } }
              - { id: f_phone, key: phone, type: phone, label: Phone number, required: true, maps_to: { mode: mapped, slot: profile.phone }, config: { country_picker: true, default_country: "{{ params.default_country }}", verify: "off" } }
              - { id: f_email, key: email, type: email, label: Email address, required: true, maps_to: { mode: mapped, slot: profile.email } }
              - id: f_product
                key: product
                type: single_select
                label: What would you like to insure?
                required: true
                maps_to: { mode: custom, key: product }
                options:
                  $each: params.products
                  $value: { value: "{{ item }}", label: "{{ item }}" }
              - { id: f_consent, key: contact_ok, type: boolean, label: I agree that you may contact me about my quote, required: true, maps_to: { mode: custom, key: contact_ok, trait: true } }
      verification_settings: { required: false }
  - ref: form_sold
    kind: form
    about: Staff-only form an advisor fills in when a customer buys. It stops the quote reminders and sends the welcome.
    when: { param: quote_source, equals: form }
    body:
      name: Policy sold
      slug: policy-sold
      description: Advisors only. One entry for each customer who buys a policy.
      draft_state:
        version: 1
        meta: { layout: classic, submit_label: Save sale, success_message: Saved. The customer will get a welcome. }
        pages:
          - id: p1
            title: Policy sold
            description: Use the phone number from the customer's quote. Do not enter a policy number or an amount.
            fields:
              - { id: f_phone, key: phone, type: phone, label: Customer's phone number, required: true, maps_to: { mode: mapped, slot: profile.phone }, config: { country_picker: true, default_country: "{{ params.default_country }}", verify: "off" } }
              - { id: f_date, key: sold_on, type: date, label: Date sold, required: true, maps_to: { mode: custom, key: policy_sold_at, trait: true }, config: { default_today: true } }
      verification_settings: { required: false }

  - ref: q_quotes_form
    kind: saved_question
    about: How many people asked for a quote on the form (used by the weekly report).
    when: { param: quote_source, equals: form }
    body:
      name: "Report: quotes requested"
      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_quote.id }}" }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: q_sold_form
    kind: saved_question
    about: How many sales advisors recorded.
    when: { param: quote_source, equals: form }
    body:
      name: "Report: policies sold"
      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_sold.id }}" }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: q_quotes_app
    kind: saved_question
    about: How many people got a quote in the app (used by the weekly report).
    when: { param: quote_source, equals: app }
    body:
      name: "Report: quotes created"
      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.quote_event }}" }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: q_sold_app
    kind: saved_question
    about: How many people bought a policy in the app.
    when: { param: quote_source, equals: app }
    body:
      name: "Report: policies purchased"
      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.purchased_event }}" }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: report_weekly
    kind: report
    about: Each week, quotes, purchases and the state of the advisor calls, to the managers.
    body:
      name: "Weekly: quotes and policies"
      description: Quotes, purchases and advisor calls.
      schedule_cron: "{{ params.report_cron }}"
      timezone: "{{ params.timezone }}"
      enabled: false
      spec:
        period: last_7_days
        compare: previous_period
        week_start: monday
        sections:
          - { id: quotes_form, kind: saved_question, title: Quotes requested, viz: number, when: { param: quote_source, equals: form }, params: { question_id: "{{ ref.q_quotes_form.id }}" } }
          - { id: sold_form, kind: saved_question, title: Policies sold, viz: number, when: { param: quote_source, equals: form }, params: { question_id: "{{ ref.q_sold_form.id }}" } }
          - { id: quotes_app, kind: saved_question, title: Quotes created, viz: number, when: { param: quote_source, equals: app }, params: { question_id: "{{ ref.q_quotes_app.id }}" } }
          - { id: sold_app, kind: saved_question, title: Policies purchased, viz: number, when: { param: quote_source, equals: app }, params: { question_id: "{{ ref.q_sold_app.id }}" } }
          - { id: tasks, kind: tasks_summary, title: Advisor calls, params: {} }
      recipients: [{ type: team, id: "{{ ref.team_managers.id }}" }]
      channel_policy: { mode: email_only }

  - ref: tmpl_quote_email
    kind: template
    about: Reminder email two hours after the quote. No amounts.
    body:
      name: Your quote is waiting (email)
      channel: email
      category: Quotes
      style: html
      status: active
      subject: "Your {{ traits.quote_product | default: \"insurance\" }} quote with {{ params.org_name }}"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "It's saved and ready when you are." } }
      variables: {}
      body: >-
        <p>Hello {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>Thank you for asking {{ params.org_name }} for a {{ traits.quote_product | default: "insurance" }}
        quote. It's saved and ready when you are.</p>
        {% if "{{ params.finish_url }}" != "" %}<p><a href="{{ params.finish_url }}">Pick up where you left off</a></p>{% endif %}
        <p>Have a question first? Call us on {{ params.claims_phone }} and an advisor will help.</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_quote_sms
    kind: template
    about: Next-day SMS reminder.
    body:
      name: Your quote is waiting (SMS)
      channel: sms
      category: Quotes
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: hello {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, your {{ traits.quote_product | default: \"insurance\" }} quote is still waiting for you.{% if \"{{ params.finish_url }}\" != \"\" %} Finish here: {{ params.finish_url }}{% endif %} Or call {{ params.claims_phone }} and an advisor will help. Reply STOP to opt out."
  - ref: tmpl_welcome_email
    kind: template
    about: Welcome email with how to claim and where the policy document is. No policy number, no amounts.
    body:
      name: Welcome and how to claim (email)
      channel: email
      category: Policies
      style: html
      status: active
      subject: "Welcome to {{ params.org_name }}"
      metadata: { email: { from: "{{ params.email_from }}", preview_text: "How to claim, and where your policy document is." } }
      variables: {}
      body: >-
        <p>Hello {{ traits.first_name | default: "{{ params.greeting_fallback }}" }},</p>
        <p>Thank you for choosing {{ params.org_name }}. We're glad to have you.</p>
        <p><strong>Your policy document</strong> is {{ params.docs_where }}. Keep it somewhere safe.</p>
        <p><strong>If you need to make a claim</strong>, call us on {{ params.claims_phone }} as soon as you can.
        Tell us what happened and we will explain what we need from you and what happens next.</p>
        <p>We will never ask for your PIN, password or a one-time code.</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_welcome_sms
    kind: template
    about: Short welcome SMS.
    body:
      name: Welcome (SMS)
      channel: sms
      category: Policies
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: welcome, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}. Your policy document is {{ params.docs_where }}. To make a claim, call {{ params.claims_phone }}. We will never ask for your PIN or any code. Reply STOP to opt out."

  - ref: wf_quote_form
    kind: workflow
    about: A quote request gets a reminder email, an SMS and then an advisor task, each only while the customer hasn't bought.
    when: { param: quote_source, equals: form }
    body:
      name: "Quote 1: follow up until they buy"
      description: Email after two hours, SMS the next day and an advisor call after two days. Each step stops if the customer has bought.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: onboarding.submitted, form_id: "{{ ref.form_quote.id }}" }
      steps:
        - { id: s1, type: action, config: { action_type: set_trait, traits: { policy_status: quoted, quote_product: "{{ properties.product | default: \"\" }}" } } }
        - { id: s2, type: delay, config: { duration: "{{ params.email_delay }}" } }
        - { id: s3, type: filter, config: { condition: { type: trait, trait_key: policy_status, cond_operator: eq, value: quoted } } }
        - { id: s4, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_quote_email.id }}" } } } }
        - { id: s5, type: delay, config: { duration: "{{ params.sms_delay }}" } }
        - { id: s6, type: filter, config: { condition: { type: trait, trait_key: policy_status, cond_operator: eq, value: quoted } } }
        - { id: s7, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_quote_sms.id }}" } } } }
        - { id: s8, type: delay, config: { duration: "{{ params.call_delay }}" } }
        - { id: s9, type: filter, config: { condition: { type: trait, trait_key: policy_status, cond_operator: eq, value: quoted } } }
        - id: s10
          type: human_task
          config:
            human_task:
              title: "Quote follow-up: {{ traits.first_name }} {{ traits.last_name }}"
              instructions: |-
                {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) asked for a {{ traits.quote_product | default: "insurance" }} quote two days ago and hasn't bought.

                Call to ask if they have questions and help them finish. Don't quote amounts in a message afterwards. Choose an outcome and add a short note.
              assignee_refs: [{ type: team, id: "{{ ref.team_advisors.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: buying, label: Will buy, intent: positive }
                - { id: needs_time, label: Needs more time, intent: neutral }
                - { id: not_interested, label: Not interested, 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: s11, type: action, config: { action_type: set_trait, traits: { quote_call: "{{ properties.task_outcome | default: \"unknown\" }}", quote_note: "{{ properties.task_note }}" } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 168
      goal: {}
      exit_conditions: []
  - ref: wf_quote_app
    kind: workflow
    about: A quote created in the app gets a reminder email, an SMS and then an advisor task; it ends when the policy is purchased.
    when: { param: quote_source, equals: app }
    body:
      name: "Quote 1: follow up until they buy"
      description: Email after two hours, SMS the next day and an advisor call after two days. It stops when the policy is purchased.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: "{{ params.quote_event }}" }
      steps:
        - { id: s1, type: action, config: { action_type: set_trait, traits: { policy_status: quoted, quote_product: "{{ properties.product | default: \"\" }}" } } }
        - { id: s2, type: delay, config: { duration: "{{ params.email_delay }}" } }
        - { id: s3, type: filter, config: { condition: { type: trait, trait_key: policy_status, cond_operator: eq, value: quoted } } }
        - { id: s4, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_quote_email.id }}" } } } }
        - { id: s5, type: delay, config: { duration: "{{ params.sms_delay }}" } }
        - { id: s6, type: filter, config: { condition: { type: trait, trait_key: policy_status, cond_operator: eq, value: quoted } } }
        - { id: s7, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_quote_sms.id }}" } } } }
        - { id: s8, type: delay, config: { duration: "{{ params.call_delay }}" } }
        - { id: s9, type: filter, config: { condition: { type: trait, trait_key: policy_status, cond_operator: eq, value: quoted } } }
        - id: s10
          type: human_task
          config:
            human_task:
              title: "Quote follow-up: {{ traits.first_name }} {{ traits.last_name }}"
              instructions: |-
                {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) got a {{ traits.quote_product | default: "insurance" }} quote two days ago and hasn't bought.

                Call to ask if they have questions and help them finish. Don't quote amounts in a message afterwards. Choose an outcome and add a short note.
              assignee_refs: [{ type: team, id: "{{ ref.team_advisors.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: buying, label: Will buy, intent: positive }
                - { id: needs_time, label: Needs more time, intent: neutral }
                - { id: not_interested, label: Not interested, 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: s11, type: action, config: { action_type: set_trait, traits: { quote_call: "{{ properties.task_outcome | default: \"unknown\" }}", quote_note: "{{ properties.task_note }}" } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 168
      goal: { event_name: "{{ params.purchased_event }}", attribution_window_hours: 720 }
      exit_conditions: [{ event_name: "{{ params.purchased_event }}" }]
  - ref: wf_sold_form
    kind: workflow
    about: When an advisor records a sale, stop the reminders and send the welcome.
    when: { param: quote_source, equals: form }
    body:
      name: "Quote 2: policy sold"
      description: Marks the customer as a policyholder, which stops the quote reminders, and sends the welcome email and SMS.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: onboarding.submitted, form_id: "{{ ref.form_sold.id }}" }
      steps:
        - { id: s1, type: action, config: { action_type: set_trait, traits: { policy_status: purchased } } }
        - { id: s2, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_welcome_email.id }}" } } } }
        - { id: s3, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_welcome_sms.id }}" } } } }
      re_entry_policy: every_time
      re_entry_cooldown_hours: 0
      goal: {}
      exit_conditions: []
  - ref: wf_sold_app
    kind: workflow
    about: When the app records a purchase, mark the customer as a policyholder and send the welcome.
    when: { param: quote_source, equals: app }
    body:
      name: "Quote 2: policy purchased"
      description: Marks the customer as a policyholder and sends the welcome email and SMS.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: "{{ params.purchased_event }}" }
      steps:
        - { id: s1, type: action, config: { action_type: set_trait, traits: { policy_status: purchased } } }
        - { id: s2, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: email, template_id: "{{ ref.tmpl_welcome_email.id }}" } } } }
        - { id: s3, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_welcome_sms.id }}" } } } }
      re_entry_policy: every_time
      re_entry_cooldown_hours: 0
      goal: {}
      exit_conditions: []

eval:
  name: 103_insurtech_quote_to_policy
  mode: task
  vertical: ""
  prompt: >-
    We're an online insurer with a quote request form, and our advisors record sales on a form.
    When someone asks for a quote, email them a reminder after 2 hours and text them the next
    day, and if they haven't bought after 2 days give an advisor a task to call. Stop as soon as
    they buy. When a policy is recorded as sold, send a welcome that explains how to claim and
    where the policy document is. Send the manager a weekly report of quotes and sales. Never put
    a premium, an amount or a policy number in a message.
  expect_artifacts: [form, workflow, message_template, report]
  forbid_terms: [premium, policy number]
