Email automation
Email agents in freight forwarding: 3h40 to 22 minutes
The ops desk used to open Outlook at 06:45 and not look up until lunch. 4,300 weekly Portbase notifications now reconcile themselves while the kettle boils.

It is 06:45 on a Tuesday in Breda. The ops desk lead pours coffee, opens her laptop, and watches the kettle reboil before the inbox finishes loading. Between 18:00 the previous evening and 06:00 this morning, Portbase pushed 612 arrival notifications into the shared arrivals@ mailbox. CargoWise One drip-fed another 480 status updates. Somewhere in that pile are six containers that need a customs broker on the phone before 09:00, three that are sitting on a quay with the wrong release reference, and one that the terminal has already lost.
This is the morning manifest scan. Until last November it took 3 hours 40 minutes, every working day, including the days when she was the only person in the office.
The morning manifest scan
Breda is forty minutes from the Maasvlakte. Our client, a 29-person freight forwarder we cannot name here, runs sea, air, and full-container loads for industrial shippers across the Benelux. Their backbone is CargoWise One, the WiseTech Global TMS that quietly runs about a third of the European mid-market. Their port-side eyes and ears come from Portbase, the Rotterdam Port Community System that funnels arrival, declaration and release messages from terminals, customs and shipping lines.
The two systems do not talk. Portbase emails. CargoWise has its eDoc inbox and an eAdaptor API, but the format mismatch means most operators copy reference numbers by hand. For the Breda team that meant five steps for every notification:
- open the Portbase arrival notification
- find the bill of lading or air waybill
- search CargoWise for the matching shipment
- verify ETA, vessel, container number, customs status
- flag anything off-pattern for the broker call at 09:00
612 emails in a night, five steps each, twelve seconds per step on the good days. The arithmetic is bleak. The arithmetic is also misleading, because the bad days are not uniformly distributed: Monday morning carries 60% of a week's weekend backlog, and the first Tuesday of the month carries the previous month's customs corrections.
Where Portbase and CargoWise disagree
If the two feeds always agreed, the agent would be a glorified rule engine. They do not. Three patterns of disagreement repeat.
Stale ETA on the carrier side. Portbase gets the actual berth time from the terminal operating system within minutes. CargoWise inherits ETA from the shipping line booking, which sometimes lags 6 to 18 hours. When the broker pulls a CargoWise screen at 08:50 he gets the old number, sends the wrong slot to the haulier, and burns a demurrage hour at the terminal gate.
Released versus release-pending. Customs sets a release in AGS, the Dutch declaration system, and Portbase mirrors it as a release notice within minutes. CargoWise sees a customs status only when the broker manually moves the shipment along. There is a window, usually around 45 minutes, where Portbase says released and CargoWise still says held. A planner reading CargoWise in good faith will tell the customer the container is stuck when it has already been cleared.
Phantom containers. When a container is restowed at Rotterdam (which happens more often than the shipping lines like to admit), Portbase emits a new arrival notification under the same bill of lading but a different visit reference. CargoWise sees this as a duplicate shipment unless someone reconciles by hand. By the end of the month the forwarder is paying twice for invoices that nobody can quite explain.
Each of these is a five-minute decision for a senior operator and a forty-minute panic for a junior. The point of the agent is not to make the decision. It is to surface the disagreement, attach the receipts, and put the right shipment in front of the right human at the right time.
The shape of the agent
We built this as a single-purpose email agent, not a general assistant. It has one inbox, three tools, and a strict output schema. The whole thing runs on a small VPS in Frankfurt with a Postgres database for state. The pseudocode is unglamorous:
async def handle_arrival(email):
parsed = parse_portbase_xml(email.body)
if not parsed.bill_of_lading:
return queue_for_human(email, reason="no_bl")
cw = await cargowise.find_shipment(
bl=parsed.bill_of_lading,
container=parsed.container_number,
)
if cw is None:
return queue_for_human(email, reason="no_match")
disagreements = compare(parsed, cw)
if not disagreements:
await cargowise.append_note(cw.id, parsed.summary())
return
severity = classify(disagreements)
user = route_to_user(cw, severity)
await draft_brief(user, parsed, cw, disagreements)
Nothing exotic. The reason the agent works is not the model choice, it is the schema. The Portbase XML envelope is well-formed and stable; the CargoWise eAdaptor objects are documented. Once you commit to those two contracts, the language model only does what it is good at: classifying severity, summarising the disagreement in two sentences, and writing the brief paragraph that lands in the operator's morning queue.
The model never decides whether to release a container. It never writes back to CargoWise except as an append-only note on a shipment that already exists. It never emails the customer. That ceiling is non-negotiable, and the system tests fail loudly if any of those surfaces appear in the agent's tool list.
AEO gating and the customs reference problem
This is the part of the build we are proudest of, and it cuts to a question every operational AI build eventually has to answer: where do you draw the line between what the model can reason about and what it cannot see at all? A model that can read your filesystem can write to it. A model that can see customer data can leak it. The only guardrail worth shipping is one the model cannot reason its way around.
Freight forwarders live this. Anyone holding AEO status (Authorised Economic Operator, the EU's trusted-trader programme) carries a quiet operational obligation: customs identifiers such as MRN numbers, AGS declaration IDs and BE/IM numbers must not be surfaced to staff outside the cleared circle. A junior planner who has been with the company three weeks should never see an MRN in their inbox, even if the agent could technically fetch it. The AEO audit happens once every three years and the auditor reads inbox samples.
The pattern we used is dumb on purpose. Customs identifiers are tagged at parse time, stripped from the payload, and held in a separate Postgres table keyed by shipment ID. The drafting prompt never receives them. The agent's brief references a shipment by its CargoWise house bill number; if the recipient needs the MRN, they click through to CargoWise, where the existing role-based access control decides whether to show it. The model literally cannot leak what it never saw.
If a regulated field can reach the model's context window, assume it will eventually reach a draft. Strip on parse, not on output. Output filters are a tripwire, not a fence.
This sounds severe and it is. We considered building a redaction step on the output side. It would have shipped two weeks earlier. It would also have failed the first time a prompt-injection arrived dressed as a Portbase notification, which on a public port community system is a matter of when, not if. The output-filter version of this agent is one cleverly worded malicious XML field away from quietly mailing a competitor your client's customs profile.
From 3h40 to 22 minutes
The headline number is honest. We measured the morning scan window between October 2025 (manual baseline, 21 working days) and April 2026 (agent live, three months bedded in, 22 working days). Median time from 06:30 inbox-open to "ops lead clears the queue" went from 220 minutes to 22 minutes. The variance also collapsed: the manual baseline had a standard deviation of 47 minutes, the agent run has 6.
What the headline number hides is more interesting. The 22 minutes is almost entirely the disagreement queue. The agent currently flags between 18 and 31 shipments per morning. Each takes the lead between 30 and 90 seconds to resolve, because the brief already contains the Portbase summary, the CargoWise state, and a one-line classification of which of the three disagreement patterns applies. She is not reading the email any more, she is approving a decision.
The remaining 580-ish notifications per morning that match cleanly never reach a human. They become append-only notes on the relevant CargoWise shipment, timestamped and traceable. If anything goes wrong downstream, the audit trail is there, and the AEO auditor sees a clean chain of custody.
The part that took us by surprise: the junior planner who used to triage the inbox now spends her first ninety minutes on customer calls. The ops director estimates this is worth roughly one full FTE of recovered capacity. The agent's running cost, including the model bill, the VPS, and the email infrastructure, is under €180 a month. Two new shippers were onboarded in Q1 without a new hire.
What this is not
It is not a general assistant. The ops desk cannot ask it questions. It does not summarise the day, draft customer updates, or rank shipments by margin. It does one job, on one mailbox, with one writeable surface (the CargoWise note field) and one tightly schemaed output. Every time we have been tempted to broaden its scope, we have walked it back. The reliability of the system is exactly the reliability of its smallest contract.
It is not autonomous in any meaningful sense. The model never moves a container, never authorises a customs release, never sends an outbound email. The brief sits in front of a human until the human acts on it.
And it is not finished. The next thing we want to add is voyage-level reconciliation: when a vessel is restowed and Portbase reissues notifications under new visit references, the agent should consolidate the lineage and present one updated shipment record rather than three confused ones. That work is scheduled for Q3 2026. We will write it up when it is live.
A version you could try this week
When we built the email agent for the Breda forwarder, the thing we kept hitting was that the cheap path (let the model decide everything) and the safe path (let the model decide nothing) both fail. We solved it by drawing the line at the data layer rather than the output layer; that work lives under AI agents at ABN.
The smallest thing you can do this week: open the last seven days of your highest-volume operational inbox, count the distinct sender domains, and tag each message as "matched cleanly" or "needed a human". The ratio between those two numbers is the only data point you need to decide whether an email agent is worth building.
Key takeaway
The reliability of an email agent equals the reliability of its smallest contract, not the cleverness of its model.
FAQ
Why not just use CargoWise One's own Portbase integration?
It exists, but the format mapping it ships with covers only the simplest arrival notification shape. Restows, partial releases and corrected MRNs still drop to manual handling, which is exactly where the morning backlog comes from.
Does the agent ever write back to CargoWise One?
Only as an append-only note on shipments that already exist. It never creates a shipment, never amends financial fields, never changes customs status. The CargoWise role used for the integration is read-only on every field except that note.
Is this approach compatible with AEO compliance?
Yes, because customs identifiers never enter the model's context window. They are stripped at parse time, stored separately, and only accessible through CargoWise's existing role-based access control. The agent has nothing to leak.
What happens when the agent can't find a CargoWise match for a Portbase notification?
It queues the email for a human with the reason tagged (no_match, no_bl, ambiguous_bl). The lead clears those first because they are usually the shipments at risk of missing a release window.