Skip to content

Automation cookbook

Every automation on this page is pulled straight from the Parcel Aggregator's examples/automations/ folder when this site is built, so it always matches the version that is actually shipped. Looking for dashboard cards? They have their own page.

They are carrier-agnostic on purpose: they trigger on canonical ParcelStatus values and the unified parcel_aggregator_* events, so the same automation covers a carrier you install next year without a single edit.

Not running the aggregator?

These work per carrier too. Swap parcel_aggregator_ for the carrier's own domain (postnl_parcel_status_changed) and the aggregator's sensors for that carrier's own. The parcel data inside is identical — see the contract.

Where these go

Settings → Automations & scenes → Create automation → Create new automation, then ⋮ → Edit in YAML and paste over what is there.

Automations

Parcels: add delivery to my calendar

Add every expected delivery — from any installed carrier — to a calendar of your choice.

# Add every expected delivery — from any installed carrier — to a calendar
# of your choice.
#
# The aggregator already ships a read-only combined "Deliveries" calendar
# entity that needs no maintenance — use that if you just want to SEE all
# carriers' deliveries together in Home Assistant. This automation is for the
# other case: writing each delivery into your OWN calendar (Google, CalDAV,
# or HA's Local Calendar) so it also shows on your phone.
#
# Pick a writable calendar below (replace `calendar.your_calendar`).
#
# Caveat: `calendar.create_event` always creates a NEW event, so if a carrier
# shifts the ETA you'll get a second entry — delete the old one by hand, or
# just rely on the built-in combined Deliveries calendar instead.

alias: "Parcels: add delivery to my calendar"
description: Write every carrier's expected delivery window into a calendar you choose.
mode: queued
max: 10

triggers:
  - trigger: event
    event_type: parcel_aggregator_parcel_delivery_time_changed

conditions:
  # Only act when there is a real start moment to put on the calendar.
  - condition: template
    value_template: "{{ trigger.event.data.new_planned_from is not none }}"

actions:
  - action: calendar.create_event
    target:
      entity_id: calendar.your_calendar
    data:
      summary: >-
        {{ trigger.event.data.carrier }}:
        {{ trigger.event.data.sender or trigger.event.data.barcode }}
      description: >-
        Status: {{ trigger.event.data.status }}
        {{ trigger.event.data.url }}
      start_date_time: "{{ trigger.event.data.new_planned_from }}"
      end_date_time: >-
        {{ trigger.event.data.new_planned_to
           if trigger.event.data.new_planned_to
           else (as_datetime(trigger.event.data.new_planned_from) + timedelta(hours=1)).isoformat() }}

View on GitHub

Parcel: announce next delivery (any carrier)

Speak the expected delivery window for the next active parcel across every carrier, one hour before the planned_from timestamp. Uses the aggregator's Next delivery sensor directly instead of an event.

# Speak the expected delivery window for the next active parcel across
# every carrier, one hour before the planned_from timestamp. Uses the
# aggregator's Next delivery sensor directly instead of an event.

alias: "Parcel: announce next delivery (any carrier)"
description: Speak the next aggregated delivery window an hour before it opens.
mode: single

triggers:
  - trigger: template
    value_template: >-
      {% set when = states('sensor.parcel_aggregator_next_delivery') %}
      {% if when not in ('unknown', 'unavailable') %}
        {{ (as_datetime(when) - now()).total_seconds() | int == 3600 }}
      {% endif %}

actions:
  - action: tts.cloud_say
    data:
      entity_id: media_player.living_room
      message: >-
        Parcel on its way, expected around
        {{ as_datetime(states('sensor.parcel_aggregator_next_delivery'))
           .strftime('%H:%M') }}.

View on GitHub

Parcel: daily morning summary

Daily 08:00 push notification summarising what's expected today across every installed carrier. Uses the aggregator's by_carrier count attribute for the headline and the parcels attribute for the sender list per carrier.

# Daily 08:00 push notification summarising what's expected today
# across every installed carrier. Uses the aggregator's `by_carrier`
# count attribute for the headline and the `parcels` attribute for
# the sender list per carrier.
#
# Set the time to whatever suits your routine — the trigger is a fixed
# time-of-day, not an event.

alias: "Parcel: daily morning summary"
description: Push the day's expected parcels grouped by carrier.
mode: single

triggers:
  - trigger: time
    at: "08:00:00"

conditions:
  - condition: numeric_state
    entity: sensor.parcel_aggregator_incoming_parcels
    above: 0

actions:
  - action: notify.mobile_app
    data:
      title: "Parcels today"
      message: >-
        {% set by_carrier = state_attr('sensor.parcel_aggregator_incoming_parcels', 'by_carrier') or {} %}
        {% set parcels = state_attr('sensor.parcel_aggregator_incoming_parcels', 'parcels') or [] %}
        {%- for carrier, count in by_carrier.items() %}
        {{ carrier }}: {{ count }}
        {%- set senders = parcels | selectattr('carrier', 'eq', carrier)
                                  | map(attribute='sender')
                                  | reject('none')
                                  | list %}
        {%- if senders %}
          • {{ senders | join('
          • ') }}
        {%- endif %}
        {%- endfor %}

View on GitHub

Parcel: out for delivery (per-carrier styling)

Notify when any parcel goes out for delivery, with a carrier-specific emoji and title. Demonstrates how to branch on the carrier field inside a single carrier-agnostic automation rather than maintaining one automation per carrier.

# Notify when any parcel goes out for delivery, with a carrier-specific
# emoji and title. Demonstrates how to branch on the `carrier` field
# inside a single carrier-agnostic automation rather than maintaining
# one automation per carrier.

alias: "Parcel: out for delivery (per-carrier styling)"
description: Notify when any parcel is on the truck today; style the message per carrier.
mode: queued
max: 10

triggers:
  - trigger: event
    event_type: parcel_aggregator_parcel_status_changed
    event_data:
      new_status: out_for_delivery

variables:
  carrier_styles:
    DHL: { emoji: "🟡", color: "yellow" }
    DPD: { emoji: "🔴", color: "red" }
    PostNL: { emoji: "🟠", color: "orange" }

actions:
  - action: notify.mobile_app
    data:
      title: "{{ carrier_styles[trigger.event.data.carrier].emoji }} {{ trigger.event.data.carrier }} on the way"
      message: >-
        {{ trigger.event.data.sender or 'Unknown sender' }} – {{ trigger.event.data.barcode }}
      data:
        color: "{{ carrier_styles[trigger.event.data.carrier].color }}"
        clickAction: "{{ trigger.event.data.url }}"

View on GitHub

Parcel: DHL parcels only

Only notify for DHL parcels — handy when you actively track one carrier and don't want noise from the others. Demonstrates the inverse of the carrier-agnostic default: a unified event listener with a carrier filter in the condition block.

# Only notify for DHL parcels — handy when you actively track one carrier
# and don't want noise from the others. Demonstrates the inverse of the
# carrier-agnostic default: a unified event listener with a carrier
# filter in the condition block.
#
# Swap "DHL" for "DPD" or "PostNL" as needed, or use `not in [...]` to
# silence specific carriers.

alias: "Parcel: DHL parcels only"
description: Notify only for new DHL parcels, ignore other carriers.
mode: queued
max: 10

triggers:
  - trigger: event
    event_type: parcel_aggregator_parcel_registered

conditions:
  - condition: template
    value_template: "{{ trigger.event.data.carrier == 'DHL' }}"

actions:
  - action: notify.mobile_app
    data:
      title: "DHL parcel on the way"
      message: >-
        {{ trigger.event.data.sender or 'Unknown sender' }} – expected
        {% if trigger.event.data.planned_from %}
        {{ as_timestamp(trigger.event.data.planned_from)
           | timestamp_custom('%a %-d %b') }}
        {% else %}
        soon
        {% endif %}
      data:
        clickAction: "{{ trigger.event.data.url }}"

View on GitHub

Parcel: ready at pickup point (any carrier)

Notify when a parcel arrives at any pickup point — DHL ServicePoint, PostNL Point, or DPD ParcelShop — ready for collection.

# Notify when a parcel arrives at any pickup point — DHL ServicePoint,
# PostNL Point, or DPD ParcelShop — ready for collection.
#
# Triggers on the canonical AT_PICKUP_POINT status. Each carrier maps
# its own raw "ready to collect" status onto this enum value, so one
# automation handles all three.

alias: "Parcel: ready at pickup point (any carrier)"
description: Notify when any parcel arrives at a pickup point for collection.
mode: queued
max: 10

triggers:
  - trigger: event
    event_type: parcel_aggregator_parcel_status_changed
    event_data:
      new_status: at_pickup_point

actions:
  - action: notify.mobile_app
    data:
      title: "Parcel ready for collection ({{ trigger.event.data.carrier }})"
      message: >-
        {{ trigger.event.data.sender }} is waiting for you at
        {{ trigger.event.data.pickup_point or 'the pickup point' }}.
      data:
        clickAction: "{{ trigger.event.data.url }}"

View on GitHub

Parcel: delivery time updated (any carrier)

Notify when any carrier updates the expected delivery time for an active parcel — either a brand-new ETA appears, or an existing one shifts. value -> null transitions (carrier dropped the ETA) are intentionally silent at the integration level, so they will not trigger this automation.

# Notify when any carrier updates the expected delivery time for an
# active parcel — either a brand-new ETA appears, or an existing one
# shifts. value -> null transitions (carrier dropped the ETA) are
# intentionally silent at the integration level, so they will not
# trigger this automation.

alias: "Parcel: delivery time updated (any carrier)"
description: Notify when any carrier updates the expected delivery time for an active parcel.
mode: queued
max: 10

triggers:
  - trigger: event
    event_type: parcel_aggregator_parcel_delivery_time_changed

actions:
  - action: notify.mobile_app
    data:
      title: "New delivery time ({{ trigger.event.data.carrier }})"
      message: >-
        {{ trigger.event.data.sender }} – {{ trigger.event.data.barcode }}:
        expected
        {{ as_timestamp(trigger.event.data.new_planned_from) | timestamp_custom('%a %d %b %H:%M', true)
           if trigger.event.data.new_planned_from else 'unknown' }}
      data:
        clickAction: "{{ trigger.event.data.url }}"

View on GitHub

Parcel: out for delivery (any carrier)

Notify exactly once per parcel when any carrier says it's on the delivery vehicle today. Filters on the canonical ParcelStatus value out_for_delivery, so this is independent of carrier-specific raw status strings.

# Notify exactly once per parcel when any carrier says it's on the
# delivery vehicle today. Filters on the canonical ParcelStatus value
# `out_for_delivery`, so this is independent of carrier-specific raw
# status strings.

alias: "Parcel: out for delivery (any carrier)"
description: Notify when any parcel goes out for delivery today.
mode: queued
max: 10

triggers:
  - trigger: event
    event_type: parcel_aggregator_parcel_status_changed
    event_data:
      new_status: out_for_delivery

actions:
  - action: notify.mobile_app
    data:
      title: "Out for delivery today ({{ trigger.event.data.carrier }})"
      message: >-
        {{ trigger.event.data.sender }} – {{ trigger.event.data.barcode }}
      data:
        clickAction: "{{ trigger.event.data.url }}"

View on GitHub

Parcels: any outgoing parcel delivered

Notify when an outgoing parcel from ANY carrier (something you sent, e.g. a webshop return) reaches the recipient, so you can watch for the refund. One automation covers every carrier via the aggregator's unified event.

# Notify when an outgoing parcel from ANY carrier (something you sent, e.g.
# a webshop return) reaches the recipient, so you can watch for the refund.
# One automation covers every carrier via the aggregator's unified event.

alias: "Parcels: any outgoing parcel delivered"
description: Notify when an outgoing/return parcel from any carrier is delivered.
mode: queued
max: 10

triggers:
  - trigger: event
    event_type: parcel_aggregator_outgoing_parcel_delivered

actions:
  - action: notify.mobile_app
    data:
      title: "Outgoing parcel delivered ({{ trigger.event.data.carrier }})"
      message: >-
        {{ trigger.event.data.receiver or trigger.event.data.barcode }} has
        arrived.
      data:
        clickAction: "{{ trigger.event.data.url }}"

View on GitHub

Parcel: new parcel registered (any carrier)

Notify when any carrier announces a new parcel.

# Notify when any carrier announces a new parcel.
#
# Uses the unified parcel_aggregator_parcel_registered event, so this one
# automation covers every carrier the aggregator knows about — no need
# for separate per-carrier copies.

alias: "Parcel: new parcel registered (any carrier)"
description: Notify when any carrier announces a new parcel.
mode: queued
max: 10

triggers:
  - trigger: event
    event_type: parcel_aggregator_parcel_registered

actions:
  - action: notify.mobile_app
    data:
      title: "New parcel on the way ({{ trigger.event.data.carrier }})"
      message: >-
        {{ trigger.event.data.sender }} – expected
        {% if trigger.event.data.planned_from %}
        {{ as_timestamp(trigger.event.data.planned_from)
           | timestamp_custom('%a %-d %b') }}
        {% else %}
        soon
        {% endif %}
      data:
        clickAction: "{{ trigger.event.data.url }}"

View on GitHub

Carrier-specific events

Every carrier fires its own events, whether or not the aggregator is installed: <domain>_parcel_registered, <domain>_parcel_status_changed, <domain>_parcel_delivered, <domain>_parcel_delivery_time_changed. Use these when you run a single carrier, when you want one carrier to behave differently from the rest, or when you need the raw carrier payload the aggregator strips. See the parcel contract for the payload.