# Origamy playbook spec v1. Reference: /playbooks/spec-format/
spec_version: 1
id: logistics.delivery-updates
title: Delivery updates and failed-delivery recovery
sector: logistics
source:
  kind: synthesized
  notes: >-
    No courier has built this with us yet. Written from the per-decision message pattern of the
    loan application follow-up (a trigger filter on one property picks the message), the
    speed-to-lead call task of the property enquiry playbook, and the rating-form pattern of the
    feedback follow-up. Couriers and dispatch riders commonly lose time on failed deliveries and
    on calls that ask "where are you?", so the messages give an ETA window and a way to
    reschedule without a call.
derived_from: base.feedback-follow-up
vocabulary:
  person: customer
  people: customers
  group: dispatch team
summary: >-
  Customers get an SMS when their parcel is out for delivery, with a time window and a number to
  call. A failed delivery sends a link to pick a new day and gives your dispatcher a task, and a
  reschedule request gives the dispatcher another. Delivered parcels get a short rating form an
  hour later, and a low rating gives your operations team a task. A weekly report shows how
  deliveries went.
when_to_use:
  - You run a courier, last-mile or dispatch-rider business, and customers keep calling to ask where their parcel is.
  - Failed deliveries (nobody home, phone off, wrong location) cost you a second trip because nobody follows up.
  - Your riders or dispatchers can log a status in your dispatch app, or on a short form if you don't have an app.

requires:
  data_in:
    - how: staff_form
      what: "The Dispatch form this playbook creates: your dispatcher or rider logs each parcel as out for delivery, delivered or failed, with the customer's phone number. Use this if you have no dispatch app."
    - how: app_events
      what: "Or your dispatch app sends an event when a parcel goes out for delivery, is delivered or fails, with a reason on failures."
    - how: public_form
      what: "The Reschedule and Rating forms this playbook creates. Customers open them from the SMS links."
  channels:
    - channel: sms
      for: Every message to customers
      needs: An SMS provider, or Origamy managed SMS after business verification (KYC).
  events:
    - "{{ params.out_event }}"
    - "{{ params.delivered_event }}"
    - "{{ params.failed_event }}"
    - onboarding.submitted
  traits:
    - delivery_status
    - last_delivery_rating
    - failed_delivery_outcome
    - reschedule_outcome

parameters:
  - key: org_name
    ask: true
    question: What should messages call your delivery company?
    label: Company name
    type: string
    example: Northwind Express
  - key: dispatch_via
    ask: true
    question: How do you record delivery status? Choose form if your dispatcher fills in a form, or app_events if your dispatch app sends events.
    label: Status source
    type: enum
    options: [form, app_events]
    default: form
  - key: dispatch_team
    ask: true
    question: Which team handles failed deliveries and reschedule requests? (your dispatchers, or one person)
    label: Dispatch team
    type: team_name
    default: Dispatch
  - key: ops_team
    ask: true
    question: Which team follows up on unhappy customers and gets the weekly report?
    label: Operations team
    type: team_name
    default: Operations
  - key: dispatch_phone
    ask: true
    question: Which number should customers call about a delivery?
    label: Dispatch phone
    type: string
    default: "{{ workspace.phone }}"
  - key: out_event
    label: Out for delivery event
    type: event_name
    default: Out For Delivery
  - key: delivered_event
    label: Delivered event
    type: event_name
    default: Delivered
  - key: failed_event
    label: Delivery failed event
    type: event_name
    default: Delivery Failed
  - key: low_score
    label: Ratings at or below this (out of 5) give operations a task
    type: number
    default: 2
  - key: rating_delay
    label: How long after delivery to ask for a rating
    type: duration
    default: 1h
  - key: failed_due_hours
    label: Hours dispatch has to follow up a failed delivery
    type: hours
    default: 4
  - key: reschedule_due_hours
    label: Hours dispatch has to confirm a reschedule request
    type: hours
    default: 4
  - key: ops_due_hours
    label: Hours operations has to call after a low rating
    type: hours
    default: 24
  - 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: report_cron
    label: Weekly operations report
    type: cron
    default: "0 8 * * 1"
  - key: greeting_fallback
    label: Greeting when a first name is missing
    type: string
    default: there

timings:
  - "Out for delivery: an SMS straight away with the time window and a number to call."
  - "Failed delivery: an SMS straight away with the reschedule link, and a task for {{ params.dispatch_team }} due in {{ params.failed_due_hours }} hours."
  - "A reschedule request: an SMS straight away and a task for {{ params.dispatch_team }}, due in {{ params.reschedule_due_hours }} hours."
  - "Delivered: a rating form by SMS {{ params.rating_delay }} later (at most once every 3 days per customer)."
  - "A rating of {{ params.low_score }} or lower: a call task for {{ params.ops_team }}, due in {{ params.ops_due_hours }} hours."
  - "Weekly operations report: {{ params.report_cron }} in {{ params.timezone }}."

customer_must:
  - "If you use the Dispatch form, give it to your dispatchers and riders and have them fill it in for every parcel: the customer's phone number, the status, and for out-for-delivery an ETA window such as \"2pm to 5pm\". If you use a dispatch app, send {{ params.out_event }}, {{ params.delivered_event }} and {{ params.failed_event }} with the customer's phone as a trait, and optional eta_window, rider_phone, sender, tracking_ref and (on failures) reason properties."
  - Never put a full address in a message. These messages show only the time window, a phone number and links. Keep addresses in your dispatch system.
  - Add people to the {{ params.dispatch_team }} and {{ params.ops_team }} teams. A task assigned to an empty team reaches nobody.
  - Publish the Reschedule and Rating forms. Their links go out in the SMS.
  - Tell customers when you collect their phone number that you will text them about their delivery.
  - Connect an SMS provider or finish business verification, switch the follow-ups and report on, and test each status with your own phone.

objects:
  - ref: team_dispatch
    kind: team
    about: Follows up failed deliveries and reschedule requests.
    body: { name: "{{ params.dispatch_team }}" }
  - ref: team_ops
    kind: team
    about: Calls unhappy customers; gets the weekly report.
    body: { name: "{{ params.ops_team }}" }

  - ref: form_dispatch
    kind: form
    about: Staff-only form where a dispatcher or rider logs a parcel as out for delivery, delivered or failed. Matched to the customer by phone.
    when: { param: dispatch_via, equals: form }
    body:
      name: Dispatch
      slug: dispatch
      description: Staff only. One entry each time a parcel goes out, is delivered or fails.
      draft_state:
        version: 1
        meta: { layout: classic, submit_label: Save status, success_message: Saved. The customer will be told by SMS. }
        pages:
          - id: p1
            title: Log a parcel
            description: Use the customer's phone number. Never type a full address here.
            fields:
              - { id: f_phone, key: phone, type: phone, label: Customer'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: Customer's first name, required: false, maps_to: { mode: custom, key: first_name, trait: true } }
              - id: f_status
                key: status
                type: multiple_choice
                label: Status
                required: true
                maps_to: { mode: custom, key: delivery_status, trait: true }
                options:
                  - { value: out_for_delivery, label: Out for delivery }
                  - { value: delivered, label: Delivered }
                  - { value: failed, label: Delivery failed }
              - { id: f_ref, key: tracking_ref, type: short_text, label: Tracking or order reference, required: false, maps_to: { mode: custom, key: tracking_ref } }
              - { id: f_sender, key: sender, type: short_text, label: "Who is it from? (optional, shown to the customer)", required: false, maps_to: { mode: custom, key: sender } }
              - { id: f_eta, key: eta_window, type: short_text, label: "Time window for out for delivery, for example 2pm to 5pm", required: false, maps_to: { mode: custom, key: eta_window } }
              - { id: f_rider, key: rider_phone, type: short_text, label: "Rider's phone (optional, shown to the customer)", required: false, maps_to: { mode: custom, key: rider_phone } }
              - id: f_reason
                key: reason
                type: single_select
                label: If it failed, why? (for your team, not shown to the customer)
                required: false
                maps_to: { mode: custom, key: reason }
                options:
                  - { value: not_home, label: Nobody to receive it }
                  - { value: phone_off, label: Customer unreachable }
                  - { value: location, label: Couldn't find the location }
                  - { value: refused, label: Refused the parcel }
                  - { value: other, label: Other }
      verification_settings: { required: false }
  - ref: form_reschedule
    kind: form
    about: The form customers open from the failed-delivery SMS to pick a new day and time window.
    body:
      name: Reschedule my delivery
      slug: reschedule-delivery
      description: Pick a new day for your delivery.
      draft_state:
        version: 1
        meta: { layout: classic, submit_label: Reschedule, success_message: "Thank you! Our dispatch team will confirm your new delivery day." }
        pages:
          - id: p1
            title: Pick a new day
            description: Use the phone number we texted.
            fields:
              - { id: f_phone, key: phone, type: phone, label: Your 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: preferred_date, type: date, label: Which day suits you?, required: true, maps_to: { mode: custom, key: preferred_date } }
              - id: f_window
                key: time_window
                type: single_select
                label: What time of day?
                required: true
                maps_to: { mode: custom, key: time_window }
                options:
                  - { value: morning, label: Morning }
                  - { value: afternoon, label: Afternoon }
                  - { value: evening, label: Evening }
              - { id: f_note, key: note, type: long_text, label: "Anything that will help the rider reach you? (you don't need to type your address)", required: false, maps_to: { mode: custom, key: reschedule_note } }
      verification_settings: { required: false }
  - ref: form_rating
    kind: form
    about: A one-page rating form with an optional comment. The score is saved on the customer.
    body:
      name: Rate your delivery
      slug: rate-delivery
      description: How was your delivery?
      draft_state:
        version: 1
        meta: { layout: classic, submit_label: Send, success_message: Thank you for telling us. }
        pages:
          - id: p1
            title: How was your delivery?
            description: It takes ten seconds.
            fields:
              - { id: f_phone, key: phone, type: phone, label: Your phone number, required: true, maps_to: { mode: mapped, slot: profile.phone }, config: { country_picker: true, default_country: "{{ params.default_country }}", verify: "off" } }
              - { id: f_rating, key: rating, type: rating, label: How would you rate your delivery?, required: true, maps_to: { mode: custom, key: last_delivery_rating, trait: true }, validation: { min: 1, max: 5 } }
              - { id: f_comment, key: comment, type: long_text, label: Anything you'd like to tell us?, required: false, maps_to: { mode: custom, key: comment } }
      verification_settings: { required: false }

  - ref: tmpl_out
    kind: template
    about: Out-for-delivery message with the time window and a number to call. No address.
    body:
      name: Out for delivery (SMS)
      channel: sms
      category: Delivery
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: hi {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, your parcel{% if properties.sender %} from {{ properties.sender }}{% endif %} is out for delivery today. Expected: {{ properties.eta_window | default: \"today\" }}. Please keep your phone on. Questions? Call {{ properties.rider_phone | default: \"{{ params.dispatch_phone }}\" }} Reply STOP to opt out."
  - ref: tmpl_failed
    kind: template
    about: Failed-delivery message with the reschedule link. No reason, no address.
    body:
      name: Delivery missed (SMS)
      channel: sms
      category: Delivery
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: sorry, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}, we couldn't deliver your parcel today. Pick a new day here: {{ ref.form_reschedule.url }} Or call {{ params.dispatch_phone }}. Reply STOP to opt out."
  - ref: tmpl_rescheduled
    kind: template
    about: Acknowledgement when a customer asks for a new day.
    body:
      name: Reschedule received (SMS)
      channel: sms
      category: Delivery
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: thank you, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}. We've got your request for {{ properties.preferred_date }}, {{ properties.time_window }}. Our dispatch team will confirm shortly. Reply STOP to opt out."
  - ref: tmpl_rate
    kind: template
    about: Rating request after delivery.
    body:
      name: Rate your delivery (SMS)
      channel: sms
      category: Delivery
      style: text
      status: active
      subject: ""
      metadata: {}
      variables: {}
      body: "{{ params.org_name }}: your parcel was delivered, {{ traits.first_name | default: \"{{ params.greeting_fallback }}\" }}. How did we do? Rate it in ten seconds: {{ ref.form_rating.url }} Reply STOP to opt out."

  - ref: q_out
    kind: saved_question
    about: Parcels out for delivery in the report's period.
    body:
      name: "Report: out for delivery"
      data_plane_id: default
      viz_type: number
      viz_settings: {}
      visibility: workspace
      query_ast:
        table: events
        filters:
          operator: and
          conditions:
            - { when: { param: dispatch_via, equals: form }, field: event_name, op: eq, value: onboarding.submitted }
            - { when: { param: dispatch_via, equals: form }, field: properties.form_id, op: eq, value: "{{ ref.form_dispatch.id }}" }
            - { when: { param: dispatch_via, equals: form }, field: properties.status, op: eq, value: out_for_delivery }
            - { when: { param: dispatch_via, equals: app_events }, field: event_name, op: eq, value: "{{ params.out_event }}" }
        aggregations: [{ fn: count, alias: parcels }]
  - ref: q_delivered
    kind: saved_question
    about: Parcels delivered in the report's period.
    body:
      name: "Report: delivered"
      data_plane_id: default
      viz_type: number
      viz_settings: {}
      visibility: workspace
      query_ast:
        table: events
        filters:
          operator: and
          conditions:
            - { when: { param: dispatch_via, equals: form }, field: event_name, op: eq, value: onboarding.submitted }
            - { when: { param: dispatch_via, equals: form }, field: properties.form_id, op: eq, value: "{{ ref.form_dispatch.id }}" }
            - { when: { param: dispatch_via, equals: form }, field: properties.status, op: eq, value: delivered }
            - { when: { param: dispatch_via, equals: app_events }, field: event_name, op: eq, value: "{{ params.delivered_event }}" }
        aggregations: [{ fn: count, alias: parcels }]
  - ref: q_failed
    kind: saved_question
    about: Failed deliveries in the report's period.
    body:
      name: "Report: failed deliveries"
      data_plane_id: default
      viz_type: number
      viz_settings: {}
      visibility: workspace
      query_ast:
        table: events
        filters:
          operator: and
          conditions:
            - { when: { param: dispatch_via, equals: form }, field: event_name, op: eq, value: onboarding.submitted }
            - { when: { param: dispatch_via, equals: form }, field: properties.form_id, op: eq, value: "{{ ref.form_dispatch.id }}" }
            - { when: { param: dispatch_via, equals: form }, field: properties.status, op: eq, value: failed }
            - { when: { param: dispatch_via, equals: app_events }, field: event_name, op: eq, value: "{{ params.failed_event }}" }
        aggregations: [{ fn: count, alias: parcels }]
  - ref: q_reschedules
    kind: saved_question
    about: Reschedule requests in the report's period.
    body:
      name: "Report: reschedule requests"
      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_reschedule.id }}" }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: q_low_ratings
    kind: saved_question
    about: Low delivery ratings in the report's period.
    body:
      name: "Report: low ratings"
      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_rating.id }}" }
            - { field: properties.rating, op: lte, value: "{{ params.low_score }}" }
        aggregations: [{ fn: distinct, field: canonical_user_id, alias: people }]
  - ref: report_weekly
    kind: report
    about: The week's parcels out, delivered and failed, reschedule requests and low ratings, sent to operations.
    body:
      name: "Weekly: deliveries"
      description: Last week's deliveries, failures, reschedule requests and low ratings, compared with the week before.
      schedule_cron: "{{ params.report_cron }}"
      timezone: "{{ params.timezone }}"
      enabled: false
      spec:
        period: last_7_days
        compare: previous_period
        week_start: monday
        sections:
          - { id: out, kind: saved_question, title: Out for delivery, viz: number, params: { question_id: "{{ ref.q_out.id }}" } }
          - { id: delivered, kind: saved_question, title: Delivered, viz: number, params: { question_id: "{{ ref.q_delivered.id }}" } }
          - { id: failed, kind: saved_question, title: Failed deliveries, viz: number, params: { question_id: "{{ ref.q_failed.id }}" } }
          - { id: reschedules, kind: saved_question, title: Reschedule requests, viz: number, params: { question_id: "{{ ref.q_reschedules.id }}" } }
          - { id: low, kind: saved_question, title: Low ratings, viz: number, params: { question_id: "{{ ref.q_low_ratings.id }}" } }
          - { id: tasks, kind: tasks_summary, title: Follow-ups, params: {} }
      recipients: [{ type: team, id: "{{ ref.team_ops.id }}" }]
      channel_policy: { mode: email_only }

  - ref: wf_out_form
    kind: workflow
    about: Dispatcher logs "out for delivery" on the form; the customer gets the ETA SMS.
    when: { param: dispatch_via, equals: form }
    body:
      name: "Delivery 1: out for delivery (form)"
      description: An SMS with the time window and a number to call when a parcel is logged as out for delivery.
      enabled: false
      trigger_type: event
      trigger_config:
        event_name: onboarding.submitted
        form_id: "{{ ref.form_dispatch.id }}"
        property_filters: [{ property: status, cond_operator: eq, value: out_for_delivery }]
        properties: { $origamy_property_filters: true }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_out.id }}" } } } }
      re_entry_policy: every_time
      re_entry_cooldown_hours: 0
      goal: {}
      exit_conditions: []
  - ref: wf_out_app
    kind: workflow
    about: Your app sends "out for delivery"; the customer gets the ETA SMS.
    when: { param: dispatch_via, equals: app_events }
    body:
      name: "Delivery 1: out for delivery (app)"
      description: An SMS with the time window and a number to call when the app marks a parcel out for delivery.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: "{{ params.out_event }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_out.id }}" } } } }
      re_entry_policy: every_time
      re_entry_cooldown_hours: 0
      goal: {}
      exit_conditions: []
  - ref: wf_delivered_form
    kind: workflow
    about: Dispatcher logs "delivered" on the form; a rating request follows an hour later.
    when: { param: dispatch_via, equals: form }
    body:
      name: "Delivery 2: delivered (form)"
      description: A short rating request after a parcel is logged as delivered.
      enabled: false
      trigger_type: event
      trigger_config:
        event_name: onboarding.submitted
        form_id: "{{ ref.form_dispatch.id }}"
        property_filters: [{ property: status, cond_operator: eq, value: delivered }]
        properties: { $origamy_property_filters: true }
      steps:
        - { id: s1, type: delay, config: { duration: "{{ params.rating_delay }}" } }
        - { id: s2, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_rate.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 72
      goal: {}
      exit_conditions: []
  - ref: wf_delivered_app
    kind: workflow
    about: Your app sends "delivered"; a rating request follows an hour later.
    when: { param: dispatch_via, equals: app_events }
    body:
      name: "Delivery 2: delivered (app)"
      description: A short rating request after the app marks a parcel delivered.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: "{{ params.delivered_event }}" }
      steps:
        - { id: s1, type: delay, config: { duration: "{{ params.rating_delay }}" } }
        - { id: s2, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_rate.id }}" } } } }
      re_entry_policy: cooldown
      re_entry_cooldown_hours: 72
      goal: {}
      exit_conditions: []
  - ref: wf_failed_form
    kind: workflow
    about: Dispatcher logs "failed" on the form; the customer gets the reschedule link and dispatch gets a task.
    when: { param: dispatch_via, equals: form }
    body:
      name: "Delivery 3: delivery failed (form)"
      description: The reschedule link by SMS and a follow-up task for dispatch when a parcel is logged as failed.
      enabled: false
      trigger_type: event
      trigger_config:
        event_name: onboarding.submitted
        form_id: "{{ ref.form_dispatch.id }}"
        property_filters: [{ property: status, cond_operator: eq, value: failed }]
        properties: { $origamy_property_filters: true }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_failed.id }}" } } } }
        - id: s2
          type: human_task
          config:
            human_task:
              title: "Failed delivery: {{ traits.first_name }} {{ traits.last_name }}"
              instructions: |-
                A delivery for {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) failed. Reference: {{ properties.tracking_ref | default: "not given" }}. Reason logged: {{ properties.reason | default: "not given" }}.

                The customer has been texted a link to pick a new day. If they haven't used it, call them to rebook, or arrange for the parcel to go back to the sender. Choose an outcome and add a short note.
              assignee_refs: [{ type: team, id: "{{ ref.team_dispatch.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: rebooked, label: Rebooked, intent: positive }
                - { id: returned, label: Returning to sender, intent: neutral }
                - { id: not_reached, label: Could not reach, intent: negative }
              due_hours: "{{ params.failed_due_hours }}"
              wait: true
              wait_timeout_hours: 72
              notify: immediate
        - { id: s3, type: action, config: { action_type: set_trait, traits: { failed_delivery_outcome: "{{ properties.task_outcome | default: \"unknown\" }}", failed_delivery_note: "{{ properties.task_note }}" } } }
      re_entry_policy: every_time
      re_entry_cooldown_hours: 0
      goal: {}
      exit_conditions: []
  - ref: wf_failed_app
    kind: workflow
    about: Your app sends "delivery failed"; the customer gets the reschedule link and dispatch gets a task.
    when: { param: dispatch_via, equals: app_events }
    body:
      name: "Delivery 3: delivery failed (app)"
      description: The reschedule link by SMS and a follow-up task for dispatch when the app marks a delivery failed.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: "{{ params.failed_event }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_failed.id }}" } } } }
        - id: s2
          type: human_task
          config:
            human_task:
              title: "Failed delivery: {{ traits.first_name }} {{ traits.last_name }}"
              instructions: |-
                A delivery for {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) failed. Reference: {{ properties.tracking_ref | default: "not given" }}. Reason logged: {{ properties.reason | default: "not given" }}.

                The customer has been texted a link to pick a new day. If they haven't used it, call them to rebook, or arrange for the parcel to go back to the sender. Choose an outcome and add a short note.
              assignee_refs: [{ type: team, id: "{{ ref.team_dispatch.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: rebooked, label: Rebooked, intent: positive }
                - { id: returned, label: Returning to sender, intent: neutral }
                - { id: not_reached, label: Could not reach, intent: negative }
              due_hours: "{{ params.failed_due_hours }}"
              wait: true
              wait_timeout_hours: 72
              notify: immediate
        - { id: s3, type: action, config: { action_type: set_trait, traits: { failed_delivery_outcome: "{{ properties.task_outcome | default: \"unknown\" }}", failed_delivery_note: "{{ properties.task_note }}" } } }
      re_entry_policy: every_time
      re_entry_cooldown_hours: 0
      goal: {}
      exit_conditions: []
  - ref: wf_reschedule
    kind: workflow
    about: A reschedule request gets an SMS and a dispatch task to confirm the new day.
    body:
      name: "Delivery 4: reschedule request"
      description: Acknowledges the request by SMS and asks dispatch to confirm the new day.
      enabled: false
      trigger_type: event
      trigger_config: { event_name: onboarding.submitted, form_id: "{{ ref.form_reschedule.id }}" }
      steps:
        - { id: s1, type: action, config: { action_type: send_message, channel_config: { mode: single, single: { channel: sms, template_id: "{{ ref.tmpl_rescheduled.id }}" } } } }
        - id: s2
          type: human_task
          config:
            human_task:
              title: "Reschedule: {{ traits.first_name }} {{ traits.last_name }}"
              instructions: |-
                {{ traits.first_name }} {{ traits.last_name }} ({{ traits.phone }}) asked for a new delivery on {{ properties.preferred_date }}, {{ properties.time_window }}.
                Note from the customer: {{ properties.note | default: "none" }}

                Check the day is possible, put the parcel on that day's route and confirm with the customer. If that day won't work, offer the nearest one. Choose an outcome and add a short note.
              assignee_refs: [{ type: team, id: "{{ ref.team_dispatch.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: confirmed, label: Confirmed, intent: positive }
                - { id: other_day, label: Offered another day, intent: neutral }
                - { id: not_reached, label: Could not reach, intent: negative }
              due_hours: "{{ params.reschedule_due_hours }}"
              wait: true
              wait_timeout_hours: 72
              notify: immediate
        - { id: s3, type: action, config: { action_type: set_trait, traits: { reschedule_outcome: "{{ properties.task_outcome | default: \"unknown\" }}", reschedule_note: "{{ properties.task_note }}" } } }
      re_entry_policy: every_time
      re_entry_cooldown_hours: 0
      goal: {}
      exit_conditions: []
  - ref: wf_low_rating
    kind: workflow
    about: A low rating gives operations a call task; the outcome is saved.
    body:
      name: "Delivery 5: low rating"
      description: When a rating at or below the unhappy line comes in, operations calls.
      enabled: false
      trigger_type: event
      trigger_config:
        event_name: onboarding.submitted
        form_id: "{{ ref.form_rating.id }}"
        property_filters: [{ property: rating, cond_operator: lte, value: "{{ params.low_score }}" }]
        properties: { $origamy_property_filters: true }
      steps:
        - id: s1
          type: human_task
          config:
            human_task:
              title: "Unhappy with delivery: {{ traits.first_name }} {{ traits.last_name }}"
              instructions: |-
                {{ traits.first_name | default: "A customer" }} rated their delivery {{ properties.rating }} out of 5 ({{ traits.phone }}).
                Their comment: {{ properties.comment | default: "none" }}

                Call, listen and put it right if you can (a re-delivery, an apology, a refund under your policy). Note what you agreed.
              assignee_refs: [{ type: team, id: "{{ ref.team_ops.id }}" }]
              strategy: round_robin
              completion_policy: first_response_wins
              outcome_options:
                - { id: resolved, label: Resolved, intent: positive }
                - { id: follow_up, label: Needs more follow-up, intent: neutral }
                - { id: not_reached, label: Could not reach, intent: negative }
              due_hours: "{{ params.ops_due_hours }}"
              wait: true
              wait_timeout_hours: 168
              notify: immediate
        - { id: s2, type: action, config: { action_type: set_trait, traits: { delivery_follow_up: "{{ properties.task_outcome | default: \"unknown\" }}", delivery_follow_up_note: "{{ properties.task_note }}" } } }
      re_entry_policy: every_time
      re_entry_cooldown_hours: 0
      goal: {}
      exit_conditions: []

eval:
  name: 109_logistics_delivery_updates
  mode: task
  vertical: ""
  prompt: >-
    We're a courier company. When our dispatcher marks a parcel out for delivery on a form, text
    the customer that it's on the way with a time window and our phone number. If a delivery
    fails, text them a link to pick a new day and give our dispatch team a task due in four
    hours. When someone submits the reschedule form, give dispatch a task to confirm it. An hour
    after a delivery, text a rating link, and if someone rates it 2 or below give our operations
    team a task to call. Never put a full address in a message. Send operations a weekly report.
  expect_artifacts: [form, workflow, message_template, report]
  forbid_terms: []
