# Explain “label created” in parcel tracking

Source: https://rasa.community/library/casebook/retail-order-status/
Author: Rod Rivera
Published: 2026-10-18T09:00:00Z

A customer asks where their order is. The latest warehouse event says a shipping label was created two days ago. That event tells the assistant something about preparation, but it does not say the parcel was delivered.

You will test a fictional order-status service and inspect the evidence behind its answer. The example also shows when to route a customer’s question for support instead of turning an old event into a delivery promise.

The first output below is a different control: it takes a delivery record and changes only its source to `warehouse`. The service must refuse that mis-sourced delivery claim. The companion separately tests the legitimate label-created event; these two inputs must not be treated as the same result.

```text
{"event_age_hours": 48, "event_source": "warehouse", "observation_age_seconds": 21599, "reason": "label_as_delivery", "status": "blocked", "support_prediction": null, "support_status": "routed"}
```

The feed observation is fresh, yet the delivery answer is refused. This real
offline output changes only the observer of a delivery record to `warehouse`.
A recent poll cannot turn a warehouse assertion into a carrier scan.

Keep the event's source and time separate from the feed's observation time.
Then give a refused status answer its own support route. The customer still
needs help when tracking cannot justify an answer.

:::callout{type="info" title="What you will test"}
Willow Shop and Larkspur Parcel are fictional. The freshness window and support
policy are assumptions for this exercise. The recorded checks run pure Python
over synthetic fixtures. The property exercise makes no Rasa, model or speech calls.
The optional microphone path uses provider APIs and is separate evidence.
:::

## Require the observer that can support the milestone

```bash
git clone https://github.com/RasaHQ/rasa-community-resources.git
cd rasa-community-resources
git checkout 4aa0c4419dc193fef7a969c12d59edcf720f2606
cd examples/mantle-voice-retail-order-status-gemini
```

The full clone also keeps the neighbouring case contract used by a unit test.
Stay in this example directory for every command below. Python's standard
library is enough for the offline path; it needs no licence, API key or package install.

::run{cmd="python3 -m unittest discover -s tests -v"}
The recorded suite completed without skips:

```text
Ran 24 tests in 0.002s

OK
```

Your timing can differ. The pinned [guard tests](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-retail-order-status-gemini/tests/test_guard.py)
check label-only status, source mismatches, stale feeds, split parcels and
support recovery. Start with `test_label_is_never_a_delivery`: the label is
answerable as a label, with both possession and delivery false.

The distinction is between a known warehouse milestone and an unsupported
claim about a carrier milestone. `label_as_delivery` also names the refusal
for a delivery record with the wrong source; it is not limited to printed labels.

The source check in [lib/orders.py](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-retail-order-status-gemini/lib/orders.py#L200-L222) is exact:

```python
def milestone_explicit(event: Optional[dict]) -> bool:
    if not event:
        return False
    milestone = event.get("milestone")
    return milestone in MILESTONE_SOURCES and event.get("source") == MILESTONE_SOURCES[milestone]

```

A label needs a warehouse source. Collection, transit and delivery need the
carrier source in the milestone mapping. Re-labelling a poll as recent leaves
that source requirement untouched. The unit test checks both a carrier label
and a warehouse delivery as invalid combinations.

The Rasa [tool wrapper](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-retail-order-status-gemini/skills/order_status/tools.py) takes only an order number
and an optional parcel number from the model:

```python

def _customer(context: Optional[ToolContext]) -> str:
    return (
        context.memory.get("project.customer_id") if context is not None else None
    ) or orders.SESSION_CUSTOMER_ID
```

```python
async def track_order(
    order_number: str, parcel_number: Optional[int] = None, context: ToolContext = None
) -> ToolResult:
    return ToolResult(llm_response=orders.track_order(_customer(context), order_number, parcel_number))
```

The customer comes from project memory in a Rasa session. Offline calls use
the synthetic session customer. Your application must supply the authenticated
customer when replacing this fixture adapter. Do not make customer identity,
tracking facts or arrival predictions model arguments.

The pinned [skill.md](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-retail-order-status-gemini/skills/order_status/skill.md) tells the model to say the
milestone, observer, event time and status reference when the tool answers.
It also requires another tool call when the caller changes parcels. This is
source evidence of the intended conversation, not a fresh model result.

## Age the observation independently of the event

The fixture in [lib/fixtures/orders.json](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-retail-order-status-gemini/lib/fixtures/orders.json) fixes its clock at
`2026-09-30T12:00:00-04:00` and its freshness window at six hours. Repeating
these checks later does not move that clock to today.

The relevant part of `facts_for` compares the observation time with that clock:

```python
    window = timedelta(hours=data["carrier_freshness_hours"])
    observed = parcel.get("carrier_observed_at")
    latest = parcel["events"][-1] if parcel.get("events") else None
    return {
        "order_subject_resolved": True,
        "carrier_event_current": bool(observed) and _parse(data["as_of"]) - _parse(observed) <= window,
        "milestone_type_explicit": milestone_explicit(latest),
    }
```

There are two questions. When did the carrier event happen? When was the feed
observed? A historical delivery can remain the latest event in a freshly
observed feed. Refreshing the event's timestamp would misstate when delivery
happened; rejecting it solely because delivery happened earlier would confuse
event age with feed freshness.

## Test the exact freshness boundary

Hold the delivery event 48 hours in the past. Vary only the observation age
and observer. This is the recorded matrix from 8 October 2026:

| Event age | Feed observation age | Event observer | Status answer         | Support route         |
| --------- | -------------------- | -------------- | --------------------- | --------------------- |
| 48 hours  | 21,599 seconds       | Carrier        | Answered              | Routed; no prediction |
| 48 hours  | 21,599 seconds       | Warehouse      | Blocked: wrong source | Routed; no prediction |
| 48 hours  | 21,600 seconds       | Carrier        | Answered              | Routed; no prediction |
| 48 hours  | 21,600 seconds       | Warehouse      | Blocked: wrong source | Routed; no prediction |
| 48 hours  | 21,601 seconds       | Carrier        | Blocked: stale feed   | Routed; no prediction |
| 48 hours  | 21,601 seconds       | Warehouse      | Blocked: stale feed   | Routed; no prediction |

At exactly six hours, the carrier observation is within the configured window.
One second later, freshness fails. On the last row, the freshness refusal
comes first; it does not make the warehouse delivery source valid.

Paste this complete exercise from the selected example directory. It mutates
a newly loaded fixture for each case, leaving the committed data untouched.
The assertions test the boundary, source and recovery path together.

```bash
python3 - <<'PY'
"""Offline exercise: run from the selected example directory."""
import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
sys.dont_write_bytecode = True
project = Path.cwd()
from lib import orders as ws

for observation_seconds in (21599, 21600, 21601):
    for event_source in ('carrier', 'warehouse'):
        data = ws.load_data()
        now = datetime.fromisoformat(data['as_of'])
        parcel = data['orders']['WS-10482']['parcels'][0]
        parcel['carrier_observed_at'] = (now - timedelta(seconds=observation_seconds)).isoformat()
        parcel['events'][-1]['at'] = (now - timedelta(hours=48)).isoformat()
        parcel['events'][-1]['source'] = event_source
        result = ws.track_order(ws.SESSION_CUSTOMER_ID, '10482', data=data)
        help_result = ws.delivery_help(ws.SESSION_CUSTOMER_ID, '10482', data=data)
        expected = 'answered' if observation_seconds <= 21600 and event_source == 'carrier' else 'blocked'
        assert result['status'] == expected and help_result['status'] == 'routed'
        assert help_result['arrival_prediction'] is None
        print(json.dumps({'observation_age_seconds': observation_seconds, 'event_age_hours': 48,
                          'event_source': event_source, 'status': result['status'],
                          'reason': result.get('reason'), 'support_status': help_result['status'],
                          'support_prediction': help_result['arrival_prediction']}, sort_keys=True))

first = ws.track_order(ws.SESSION_CUSTOMER_ID, '10539', 1)
second = ws.track_order(ws.SESSION_CUSTOMER_ID, '10539', 2)
assert first['status_reference'] != second['status_reference']
print(json.dumps({'parcel_1': first['milestone'], 'parcel_2': second['milestone'],
                  'distinct_references': first['status_reference'] != second['status_reference']}, sort_keys=True))
PY
```

Actual recorded stdout from the same exercise logic:

```text
{"event_age_hours": 48, "event_source": "carrier", "observation_age_seconds": 21599, "reason": null, "status": "answered", "support_prediction": null, "support_status": "routed"}
{"event_age_hours": 48, "event_source": "warehouse", "observation_age_seconds": 21599, "reason": "label_as_delivery", "status": "blocked", "support_prediction": null, "support_status": "routed"}
{"event_age_hours": 48, "event_source": "carrier", "observation_age_seconds": 21600, "reason": null, "status": "answered", "support_prediction": null, "support_status": "routed"}
{"event_age_hours": 48, "event_source": "warehouse", "observation_age_seconds": 21600, "reason": "label_as_delivery", "status": "blocked", "support_prediction": null, "support_status": "routed"}
{"event_age_hours": 48, "event_source": "carrier", "observation_age_seconds": 21601, "reason": "stale_carrier_event", "status": "blocked", "support_prediction": null, "support_status": "routed"}
{"event_age_hours": 48, "event_source": "warehouse", "observation_age_seconds": 21601, "reason": "stale_carrier_event", "status": "blocked", "support_prediction": null, "support_status": "routed"}
{"distinct_references": true, "parcel_1": "delivered", "parcel_2": "in_transit"}
```

Now change the event age alone from 48 hours to another historical age. Predict
whether the six status results should change before running it again. The
freshness predicate reads `carrier_observed_at`, so event age alone does not
change that decision.

:::checkpoint{id="retail-two-clocks" question="A recent feed poll contains a warehouse record marked delivered. What can support a delivery answer?" options="The recent poll is enough,A carrier delivery event with a current feed observation,Any record whose event time is recent" answer="1"}
Freshness and milestone source are separate facts. Neither substitutes for
the other, and the parcel must also belong to the signed-in customer.
:::

## Keep every answer scoped to one parcel

The last line of the exercise reads the same split order twice, selecting a
different parcel each time. Their milestones and references differ:

| Fixture order | Selected parcel | Milestone  | Reference relationship  |
| ------------- | --------------- | ---------- | ----------------------- |
| WS-10539      | 1: desk chair   | Delivered  | Different from parcel 2 |
| WS-10539      | 2: cushion set  | In transit | Different from parcel 1 |

The companion `test_each_parcel_has_its_own_status` confirms those values.
Without a parcel selection, a split order returns `parcel_not_selected`.
Resolve that question before describing either parcel's status. If a caller
asks about the other parcel, call the tracking tool again; carrying over the
first result can produce an answer that is true for the wrong item.

The status reference belongs to the selected result. Preserve it with the
milestone and source when displaying or logging the answer. Do not build an
order-wide delivery flag by taking the first delivered parcel you find.

## Let refusal lead to support without a prediction

The [delivery_help function](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-retail-order-status-gemini/lib/orders.py#L341-L374) retains ownership and parcel
resolution checks. It can route a selected customer's parcel when tracking
is stale or its latest milestone is unusable. It takes the last explicit
event, if one exists, and returns `arrival_prediction: None`.

The wrapper maps the skill's `open_delivery_help` tool to this function and
returns its payload as a `ToolResult`. The support reference and review step
come from that payload. They are not permission to promise a new arrival time.

:::diagram{title="Status refusal preserves a separate support route"}

```dot
rankdir=LR;
subject [label="Customer owns selected parcel?"];
wrong [label="Ask for correct order or parcel", class="blocked"];
fresh [label="Current feed observation?"];
source [label="Expected source for milestone?"];
answer [label="Answer this parcel's status", class="ok"];
refuse [label="Refuse current status", class="blocked"];
help [label="Route support: last explicit event; no prediction", class="ok"];
subject -> wrong [label="no"];
subject -> fresh [label="yes"];
fresh -> source [label="yes"];
fresh -> refuse [label="no"];
source -> answer [label="yes"];
source -> refuse [label="no"];
refuse -> help [label="customer requests help"];
```

:::

The support route does not weaken the status check. It answers a different
question: where can this customer's parcel problem go next? The matrix proves
that distinction for the fixture cases. A real service still needs its own
support adapter, current observation clock and authenticated customer binding.
The exercise establishes no production delivery accuracy or behaviour across
restarts and concurrent requests.

:::solution{title="Show the checks for a refused tracking answer"}
Inspect the selected order and parcel first. Then compare the feed observation
time with the configured window, without changing the event time. Check the
milestone's expected source. If status is refused, request support for that same
parcel and inspect its reference, last explicit event and absent prediction.
A support route should not reuse a refused estimate.
:::

## Check the project with the pinned SDK

A separate offline check on 8 October 2026 used Rasa 3.21.0.dev5. Its full
suite passed without skips, and project validation loaded the tools and skills.
The test summary was:

```text
Ran 24 tests in 0.002s

OK
```

Validation also printed:

```text
validate_project: ok
```

After installing this selected project, repeat those checks with:

```bash
uv run --locked python -m unittest discover -s tests -v
make validate
```

This adds evidence for SDK imports and project validation. It does not run
training, a model or speech providers. The independent property exercise above
still tests pure Python rather than engine confirmation or spoken behaviour.

## Try the configured microphone path separately

The public [README and Makefile](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-retail-order-status-gemini/README.md) document the optional Rasa run.
Use Python 3.12 and uv in this example directory:

```bash
uv sync --locked --python 3.12
make env
```

Fill the local `.env` with `RASA_LICENSE`, `GEMINI_API_KEY`,
`DEEPGRAM_API_KEY` and `RIME_API_KEY`. The project pins
Rasa Pro `3.21.0.dev5`. Its [integrations.yml](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-retail-order-status-gemini/integrations.yml) selects
`gemini-3.8-flash`, Deepgram `flux-general-en` input and Rime `mistv3` output
with the `lagoon` voice. The Inspector channel is enabled.

```bash
make validate
make train
make inspect
```

Allow microphone access. This path uses billed provider APIs and was not run
for the offline receipts above. For a bounded manual check, say “Where is
order one zero five one seven?” Then ask “So it has been delivered?” These
are proposed caller lines. Inspect that the tool result is `label_created`,
with possession and delivery false, and that the spoken answer preserves that
meaning. In a separate short check, ask about order `10560` and request help;
look for a support reference rather than a newly predicted arrival date.

Use the [quickstart](/quickstart/) if you need the general Rasa setup first.
Take observations from these calls as individual acceptance checks, rather than
as a reliability rate or evidence about other speech configurations.

:::cta{href="/library/tutorials/guarding-irreversible-actions/" label="Apply the same checks to actions that change customer data"}
Keep the trusted facts inside your service function and test the refused path
before connecting it to a real backend.
:::