PostNL

Teaching a Shipping Agent When to Stop Talking, fewer turns, less typing, faster to a label

Our first agent for creating a PostNL shipment was great at understanding what people wanted to send, but turned into a long, typed questionnaire as soon as it needed details. I designed and built a prototype in which the agent keeps the conversation for intent and context, and switches to purpose-built widgets, prefilled with what it already knows, for the structured parts.

My role

UX & conversational design, agent & front-end development, and research

Team

Product owner, business stakeholders

Timeline

1 sprint (2 weeks)

Delivery

2026

Why agentic

At PostNL we had been exploring agentic solutions that go beyond customer service: agents that guide people through a complicated or unfamiliar process. Creating a shipment was one of those use cases, because the current flow asks a lot of the customer:

  • A different product portfolio for every country. What you can send, and how, changes with the destination.

  • Conditional logic. the flow is action-driven, so specific products only appear if you made the right choices earlier on.

  • Wrong size and weight. People pick the wrong size and weight, by accident or on purpose, which leads to delays.

  • A complicated customs form. It asks for a lot of information, including an HS code customers have to find themselves.

  • Missed cross- and upsell. PostNL misses the chance to offer something relevant at the right moment.

An agent could turn this around: customers describe what they need in their own words, and we find the right product for them.

The problem we created

So that's what we built first. After a couple of tests it was clear the agent worked wonders for capturing intent and fetching additional context. But the longer the flow, the more it got in the way:

  • Longer flows became a questionnaire. Every missing detail meant another typed question.

  • Simple questions got harder. When there were only a few options, typing an answer added cognitive load instead of removing it.

  • More room for error. Everything went through an open text field, which left room for unnecessary mistakes.

Key insight: conversation is the right way to capture intent, but the wrong way to collect structured details.

Goal: find an interaction model that adapts the interaction, and with it the cognitive load, to what each step needs.

Constraints:

  • No engineering capacity, so I had to build it myself.

  • It had to fit PostNL's conversational-first vision and the tech stack chosen for it.

  • There was internal scepticism, so it had to prove it was technically possible and user friendly for customers, not just look good in Figma.

Discovery & validation

Exploring interaction models

The goal scoped the exploration well. I explored different technologies and turned them into simple visual concepts, each with pros and cons for the user and for PostNL as an organisation.

Because the topic was so technical and had to fit our architecture and vision, I mainly discussed these concepts with technical stakeholders. Our vision was conversational-first, and our tech stack had been chosen with that in mind.

Decision: stay conversational, and enhance the conversation with dynamic suggestions: interactive components that appear in the chat when they're the quicker way to answer.

First sketches

To get from concept to sketch, I mocked the happy path in Figma with our design system components. I based it on the earlier text-only agent, and added action- and form-driven components at the moments they would help most. To keep this as goal-oriented as possible, I clustered insights and learnings from earlier related research, by me and by other designers.

After a design sprint of a few days, I collected feedback from the most important people on our stakeholder map, and the flow took decent shape.

Assembling a tech stack

The goal was to test the concept with users in a realistic setting, and to prove to stakeholders that it was technically possible. That meant going beyond Figma screens: a flow-based prototype can't show how an agent decides what to do next. And there was no engineering capacity to support me.

So I assembled a stack that was capable enough to work agentically and prove the principle, yet light enough to build myself at short notice:

  • Figma Make for the front end. I could reuse a front-end agent I had built there earlier, and hosting was free.

  • Tambo for the generative UI. It was exactly the lightweight option I needed, and free for a proof of concept like this.

  • Claude Code to write the agent skills and work on the code alongside Figma Make.

The front-end agent in Figma Make was a coding agent I had built myself, based on our design system documentation in zeroheight. A central guidelines file tells it what to read first, and it fetches the right component, icon and layout guidelines depending on the prompt. Strict rules keep it on-brand: design system components instead of custom HTML, design tokens instead of hardcoded colours or pixel values, and no guessed icon names. Even designs I imported from Figma were treated as a visual reference and rebuilt with real design system components. That's what let me build an on-brand front end in a week, without a front-end developer.

Snippet of the front-end agent I build:

# Guidelines

Main hub for the Stamp design system (`@design-system/*`). Read the companion files as directed below before generating UI.

## Reading order

**Always read first:**
- `Guidelines.md` — this file; rules and entry point
- `setup.md` — what is already configured; Figma import file rules
- `tokens.md` — color tokens, component prop enums, quirks

**Read on demand:**

# Guidelines

Main hub for the Stamp design system (`@design-system/*`). Read the companion files as directed below before generating UI.

## Reading order

**Always read first:**
- `Guidelines.md` — this file; rules and entry point
- `setup.md` — what is already configured; Figma import file rules
- `tokens.md` — color tokens, component prop enums, quirks

**Read on demand:**

# Guidelines

Main hub for the Stamp design system (`@design-system/*`). Read the companion files as directed below before generating UI.

## Reading order

**Always read first:**
- `Guidelines.md` — this file; rules and entry point
- `setup.md` — what is already configured; Figma import file rules
- `tokens.md` — color tokens, component prop enums, quirks

**Read on demand:**

First working version

The first build session was about getting to something workable, not something finished: a first version of the flow that actually ran, with a live agent and real widgets. That gave stakeholders something to shoot at once again, but this time a working prototype instead of Figma screens, so they could react to how the agent really behaved. Their feedback, for example on whether the pricing and product logic matched how PostNL actually works, went straight into the next iteration.

Solution

Action-driven, with dynamic suggestions

After a short week of building, I had an on-brand, pixel-perfect prototype with agentic capabilities. It ran on three skills plus a set of general instructions, and let users describe what they were shipping in their own words.

When users gave enough information upfront, the agent moved straight on. When they didn't, the follow-up questions went beyond conversation: the agent picked the interaction that fitted the need, like tappable options for a simple choice or a short form for detailed input. Typing was always still possible.

Take: "I want to send a pair of sneakers to my sister in Berlin."

  • That settles the destination, and the item says enough to estimate size and weight.

  • The agent states both assumptions in one sentence, then goes straight to the one thing still open, how fast it should arrive, with a delivery-time widget.

Two demo flows with different prompts:

Full concept explained in slides:

Pre-defined widgets

PostNL needs very specific information to complete a shipment, so it made little sense to give the model freedom at component level. Instead, it worked with eleven documented and designed widgets, from destination and delivery time to the customs form, addresses and the order summary. The model decided when a widget was needed, never what it looked like.

One rule in the general instructions guided that choice: for every missing detail, the agent either assumes it and says so in one sentence, confirms it with a single yes/no question when a wrong guess would change the price, or asks by rendering the widget. So widgets were skipped when the information was already there. When it was only partly there, the widget came prefilled with what the agent already knew.

Knowledge in skills

Without technical support there was no backend to connect to, so all knowledge lived in agent skills. I split it logically into three sub-skills (shipping advice, customs and shipment details) so the model could fetch what it needed for each phase effectively. I wrote the pricing and product logic myself (with assists from Claude Code), based on PostNL's pricing booklet: straightforward, but effective for a prototype. As a result the prototype needed no API calls, and users still saw real prices and product rules.

Skill snippet:

## Your One Job Each Turn: Assume, Confirm, or Ask

This is the most important thing in these instructions. Everything else is detail. Every time you need a piece of information you don't yet have, you make one of three moves — and getting this choice right, consistently, is what makes you feel like an expert instead of a form.

**ASSUME** — state your best inference in one short sentence and move straight on. Use this whenever a lookup table or an explicit signal from the user already settles the answer. You are not guessing wildly; you are applying knowledge the user expects you to have. Examples:
- The user names an item → you know its typical weight and size (see the estimation table). *"Een MacBook weegt doorgaans zo'n 2,5 kg — dat wordt een standaardpakket."*
- The user signals timing → *"geen haast / maakt niet uit"* means eco; *"morgen / snel / met spoed"* means fast.
- The user names a purpose → *"cadeau / verjaardag"* means a gift; *"terugsturen"* means a return.
- The item is a physical good → the customs reason is never "Documents".
- The user names a country or city → that's the destination.

**CONFIRM** — ask one short plain-text yes/no question, no card. Use this when you have a solid estimate but it lands right on a boundary that changes the product or the price, so guessing wrong would cost the user money or a redo. Example: an unlisted item you estimate at 2.8–3.2 kg straddles two parcel tiers — *"Zit je pakket rond de 3 kg of eronder?"*

**ASK** — render the matching component card. Use this only when there is genuinely no signal to go on, so the user faces a real open choice. The card is the right tool for a genuine choice — never for confirming something you already know.

**Why this matters:** every needless question signals you haven't listened, and every silent wrong assumption erodes trust. So lean toward ASSUME when your confidence is high, drop to CONFIRM when a boundary makes the cost of being wrong real, and reserve ASK (the card) for true unknowns. When you assume, always say so in one sentence — that single sentence is what lets the user correct you cheaply.

**The trap to avoid:** don't state an assumption *and* then also render its card. If you've written *"Je stuurt naar Duitsland"* or *"ik ga uit van eco"*

## Your One Job Each Turn: Assume, Confirm, or Ask

This is the most important thing in these instructions. Everything else is detail. Every time you need a piece of information you don't yet have, you make one of three moves — and getting this choice right, consistently, is what makes you feel like an expert instead of a form.

**ASSUME** — state your best inference in one short sentence and move straight on. Use this whenever a lookup table or an explicit signal from the user already settles the answer. You are not guessing wildly; you are applying knowledge the user expects you to have. Examples:
- The user names an item → you know its typical weight and size (see the estimation table). *"Een MacBook weegt doorgaans zo'n 2,5 kg — dat wordt een standaardpakket."*
- The user signals timing → *"geen haast / maakt niet uit"* means eco; *"morgen / snel / met spoed"* means fast.
- The user names a purpose → *"cadeau / verjaardag"* means a gift; *"terugsturen"* means a return.
- The item is a physical good → the customs reason is never "Documents".
- The user names a country or city → that's the destination.

**CONFIRM** — ask one short plain-text yes/no question, no card. Use this when you have a solid estimate but it lands right on a boundary that changes the product or the price, so guessing wrong would cost the user money or a redo. Example: an unlisted item you estimate at 2.8–3.2 kg straddles two parcel tiers — *"Zit je pakket rond de 3 kg of eronder?"*

**ASK** — render the matching component card. Use this only when there is genuinely no signal to go on, so the user faces a real open choice. The card is the right tool for a genuine choice — never for confirming something you already know.

**Why this matters:** every needless question signals you haven't listened, and every silent wrong assumption erodes trust. So lean toward ASSUME when your confidence is high, drop to CONFIRM when a boundary makes the cost of being wrong real, and reserve ASK (the card) for true unknowns. When you assume, always say so in one sentence — that single sentence is what lets the user correct you cheaply.

**The trap to avoid:** don't state an assumption *and* then also render its card. If you've written *"Je stuurt naar Duitsland"* or *"ik ga uit van eco"*

## Your One Job Each Turn: Assume, Confirm, or Ask

This is the most important thing in these instructions. Everything else is detail. Every time you need a piece of information you don't yet have, you make one of three moves — and getting this choice right, consistently, is what makes you feel like an expert instead of a form.

**ASSUME** — state your best inference in one short sentence and move straight on. Use this whenever a lookup table or an explicit signal from the user already settles the answer. You are not guessing wildly; you are applying knowledge the user expects you to have. Examples:
- The user names an item → you know its typical weight and size (see the estimation table). *"Een MacBook weegt doorgaans zo'n 2,5 kg — dat wordt een standaardpakket."*
- The user signals timing → *"geen haast / maakt niet uit"* means eco; *"morgen / snel / met spoed"* means fast.
- The user names a purpose → *"cadeau / verjaardag"* means a gift; *"terugsturen"* means a return.
- The item is a physical good → the customs reason is never "Documents".
- The user names a country or city → that's the destination.

**CONFIRM** — ask one short plain-text yes/no question, no card. Use this when you have a solid estimate but it lands right on a boundary that changes the product or the price, so guessing wrong would cost the user money or a redo. Example: an unlisted item you estimate at 2.8–3.2 kg straddles two parcel tiers — *"Zit je pakket rond de 3 kg of eronder?"*

**ASK** — render the matching component card. Use this only when there is genuinely no signal to go on, so the user faces a real open choice. The card is the right tool for a genuine choice — never for confirming something you already know.

**Why this matters:** every needless question signals you haven't listened, and every silent wrong assumption erodes trust. So lean toward ASSUME when your confidence is high, drop to CONFIRM when a boundary makes the cost of being wrong real, and reserve ASK (the card) for true unknowns. When you assume, always say so in one sentence — that single sentence is what lets the user correct you cheaply.

**The trap to avoid:** don't state an assumption *and* then also render its card. If you've written *"Je stuurt naar Duitsland"* or *"ik ga uit van eco"*

How it tackled the original problems

Problem

In the prototype

Different products per country, hidden behind conditional logic

Customers described what they were sending; the agent worked out the right product from destination, contents and speed

Wrong size and weight

Estimated from the item description, with a quick check only when it sat close to a price boundary

Complicated customs form and HS codes

The agent filled in the form from the conversation and suggested the three closest HS codes

Missed cross- and upsell

Add-ons like insurance were offered when relevant, for example for valuable items

Impact

I validated the concept in moderated user tests. Customers had to ship a specific product, without any scripted steps: they could type anything, and the agent responded live. In short, it delivered what we were looking for.

User experience

  • The power of intent. Early in the conversation, we could really see the strength of the LLM in understanding intent and fetching context.

  • No loss of speed on the deterministic parts. Users said the widgets felt very familiar, and they liked recognising patterns they already knew.

  • Seamless switching. Before the test, we were sceptical about switching between chatting and filling in widgets. Users moved seamlessly between "question mode" and "work mode".

  • It worked for different user groups. Confident users could do it in one prompt, and less AI-savvy users still completed the flow in noticeably less time than with the text-only agent. In that sense, it lowered the bar for people less comfortable with AI.

Technical signals

The focus was on the user perspective, but we could also draw some early technical conclusions:

  • It works technically. The prototype ran end to end on a lightweight stack, built without engineering capacity.

  • Fewer turns offset the widget cost. Rendering widgets costs tokens, but that was levelled out by needing far fewer turns.

  • Fixed formats save tokens. Structured inputs meant fewer free-text messages, which saved the tokens otherwise spent translating what users typed into parameters.

Compared with the text-only agent


Text-only agent

With generative UI

Cognitive load

Heavy all the way through

Significantly less when context fetching was done

Capturing intent and context

Strong

Just as strong

Structured details

Typed in open input, one question at a time

Widgets with familiar patterns, skipped or prefilled where possible

Less AI-savvy users

Slowed down by a long questionnaire

Completed the flow in noticeably less time

Conversation length

Many turns

Far fewer turns

Full insight report

Learnings

The results were positive, but the tests also showed what needs work before this goes to production:

  • A conversation can still feel long. The flow was more efficient than before, but the natural timeline of a conversation sometimes made it feel long.

  • Chat text gets skipped in work mode. People had different mental models. While using the widgets they switched to scanning and doing, and often overlooked the supporting chat text.

  • No sense of progress. People missed the progress or time indicator a normal stepper has, and wondered how long they would need to keep going.

  • Widgets for editing, too. Users liked filling in widgets, but missed them when they wanted to change something they had already given. Typing the change still worked, but felt inefficient: you had to prompt precisely to change only that one thing.

  • Accessibility is a blind spot. WCAG needs special attention in the next iteration. A conversation unfolds over time instead of sitting still like a static page, which can cause major accessibility issues.

Most of these point the same way: keep the conversation, but borrow more structure from a classic flow, like visible progress and editable answers.