DPD Claim System
The DPD Temporal workflow handles automated lost-parcel claims for DPD and DPD Local. A single durable workflow manages email submission, response pattern-matching, investigation timing, and escalating reminders — all backed by Temporal's retry and replay guarantees.
Architecture Overview
DPD is fully automated — no manual intervention is required. A Temporal schedule (dpd-daily-backfill) fires every 4 hours and runs DPDBackfillOrchestratorWorkflow, which:
- Queries the database for in-flight DPD claims under 20 days old
- Skips any claim that already has a running workflow
- Starts a fresh
DPDClaimWorkflowfor everything else - Runs a cleanup pass — any running workflow whose claim has already reached a terminal status in the database is terminated
Claims are picked up within 4 hours of being created with no human involvement. The CLI backfill command (claimit backfill dpd) still exists for on-demand runs, forced recreates, or targeting specific tracking numbers, but it is not needed for normal operation.
Phase 1 — Pre-Submission Delay
Real-time claims wait 1 hour before submitting to allow supporting evidence to be attached. Backfill claims skip this delay entirely (they are already old enough). Test mode collapses the delay to 1 second.
Phase 2 — Claim Submission
The SubmitClaimToDPD activity emails the claim to the DPD recipient address.
Retry policy: 3 attempts max, 10s initial interval, 2× backoff. If the claim was already submitted (idempotency check), the workflow completes with status "Skipped".
Phase 3 — Post-Submission Setup
After a successful submission, two activities populate the workflow state before the polling loop begins.
SearchDPDEmails scans 60 days of email history for this tracking number and extracts:
- Latest inbound email ID (for reply threading)
- Client mailbox address (for sending reminders)
- Courier reminder email address (e.g.
elite@dpd.co.uk) - Number of reminders already sent (corrects state after a continue-as-new)
ExtractInvestigationDays parses the DPD acknowledgement email body to find the stated investigation window. It tries multiple regex patterns in order (e.g. "X days to investigate", "allow X days", "contacted you within X days") and sanity-checks the result to the 1–30 day range. The default fallback is 7 days.
If an inbound email from the courier is detected after the investigation email was sent, the workflow sets
CourierRespondedAfterInvestigation = trueand skips the reminder loop entirely.
Phase 4 — Email Polling Loop
The workflow waits up to 30 days for inbound emails. Each email is matched against six DPD patterns in priority order.
DOR Sub-Flow
The Denial of Receipt (DOR) flow is unique to DPD and can extend the investigation period significantly.
| Event | Activity | Status Transition | Effect on Timer |
|---|---|---|---|
| DOR Sent | ProcessDorSentActivity |
→ DorSubmittedByCourier | No change |
| DOR Received | ProcessDorReceivedActivity |
→ DorReceivedByCourier | Investigation timer restarts from this email's date |
| DOR Not Received | ProcessDorNotReceivedActivity |
→ Rejected | Workflow completes |
Unsuccessful Claim — Rejection Reason Extraction
When DPD sends an "unsuccessful" email, the activity extracts the rejection reason via pattern matching:
| Keyword in email | Rejection Reason |
|---|---|
| "timescale" | Outside claim window |
| "insufficient", "photographs", "documents" | Insufficient evidence |
| "denial of receipt", "not received" | Items not received by courier |
| "delivered" | Item confirmed as delivered |
| No keyword match | XPath parse of HTML structure |
Coat Paints special case: If the rejection reason is "No liability accepted for items carried" and the client is Coat Paints, the claim transitions to Challenging instead of Rejected. An automated challenge email is sent to
investigations@dpd.co.ukciting their liquid goods contract.
Portal Submission
When DPD requests form submission via a Salesforce link, ProcessPortalSubmissionActivity launches a headless browser, fills and submits the form, captures the confirmation number, and transitions the claim to SubmittedSent. Timeout is 10 minutes to accommodate browser automation.
No Match
Emails that don't match any pattern are acknowledged and logged. If the email is inbound (from the courier), LastInboundEmailSubject is updated for reminder threading.
Phase 5 — Reminder Loop
If no terminal response arrives after the investigation period, the workflow sends up to 3 escalating reminders. Each reminder requires human approval from the operations team before sending.
Reminder #3 CC's a fixed set of DPD management contacts (defined in DPDWorkflowConstants.cs). All reminders reply-thread to the original submission email using ReplyToEmailId and ReplyFromMailbox.
Completion Conditions
Workflow Durability
Continue-as-new is used periodically to reset Temporal's event history size. The ClaimSubmissionAttempted flag in workflow state prevents a duplicate submission email from being sent after a continue. All processed email message IDs are tracked to prevent double-processing if an email signal is delivered more than once.
Dry-run mode is supported across all activities — they log what would happen without writing to the database or sending emails. This allows safe testing of workflow logic in production Temporal.
Open Questions & Edge Cases
The following edge cases have been identified during review. Some represent areas where the system degrades gracefully without hard failure, but the behaviour may not match expectations in all scenarios.
Workflow Timeout — No Status Transition
When the 30-day email timeout is reached, the workflow completes but the claim's database status is not automatically transitioned. A claim in Investigating or DorSubmittedByCourier at timeout will remain in that status indefinitely. These claims require manual review to close out.
Denied Reminders Are Permanent
If an ops team member denies a reminder (rather than leaving it pending), the denial is logged and the workflow moves on. There is no retry, re-queue, or notification that the reminder was skipped. The claim continues waiting in the email loop until the next email arrives or the workflow times out.
Credit Amount Not Validated Against Claimed Amount
When the "All Sorted" email arrives, the credit amount is extracted from the email body and stored as-is. There is no comparison against the original claimed amount. A partial credit (e.g. £50 on a £200 claim) would be recorded as a successful credit with no flag or alert.
Credit Amount Extraction Failure Is Silent
If the credit amount cannot be extracted from the email (no regex or XPath match), the claim still transitions to Credited — but CreditAmount is left null in the database. The workflow completes successfully. No error is raised and no notification is sent.
Portal Submission Skipped for Mid-Flow Statuses
ProcessPortalSubmissionActivity only runs when the claim is in Investigating status. If DPD sends a Salesforce portal link while the claim is in DorSubmittedByCourier or another mid-flow status, the activity skips silently. The portal link is acknowledged as a no-match email and no submission attempt is made.
Challenging Status Has No Dedicated Resolution Path
After a Coat Paints challenge email is sent, the workflow remains open and continues monitoring for emails. However, there is no dedicated email pattern for a challenge response. If DPD's reply to the challenge doesn't match one of the six standard patterns, it will be logged as "no match" and the claim will remain in Challenging until a subsequent email triggers a transition or the workflow times out.
Coat Paints Challenge Logic Is Hardcoded
The client eligibility check for the auto-challenge behaviour is a string comparison against the client name "Coat Paints". Adding another client with a similar need (e.g. a different liquid goods shipper receiving the same DPD rejection) requires a code change rather than a configuration update.
Suggested Improvements
These are forward-looking ideas we could pick up as engineering tasks. None are urgent blockers — the system handles all of these gracefully today — but addressing them would improve observability and correctness over time.
Transition claims to a reviewable status on timeout. When the 30-day email timeout expires, we could automatically move the claim to a ManualReview or TimedOut status rather than leaving it in whatever state it was in. This would make it straightforward to query and action these claims without digging into Temporal history.
Re-queue or surface denied reminders. If an ops team member denies a reminder, we could log a Slack notification or create a follow-up intervention rather than silently dropping it. Even a simple audit log entry at a higher severity would make it easier to spot claims that have stalled due to a denied reminder.
Validate credit amount against claimed amount. When processing an "All Sorted" email, we could compare the extracted credit to Claim.ClaimAmount and flag any discrepancy — either via a Slack alert or a new PartialCredit status — rather than silently recording whatever DPD sends. This would catch partial settlements before they're closed out.
Alert on credit amount extraction failure. If the credit amount can't be extracted from an "All Sorted" email, we could post a Slack alert in addition to the existing log message. A claim transitioning to Credited with a null amount is worth knowing about immediately.
Handle portal submission links for mid-flow statuses. If DPD sends a Salesforce portal link while the claim is in DorSubmittedByCourier or similar, we could widen the eligible statuses in ProcessPortalSubmissionActivity or queue the link for retry once the claim returns to a compatible state, rather than silently skipping it.
Make the Coat Paints challenge configurable. The client name check is a string literal today. We could move this to a per-client configuration flag (e.g. HasLiquidGoodsContract) so other clients can opt into the same behaviour without a code change.
Key Files
| Component | Path |
|---|---|
| Workflow | EmailProcessing.Workflows/Couriers/DPD/DPDClaimWorkflow.cs |
| Submission | EmailSubmission/Couriers/DPD/SubmitClaimToDPDActivity.cs |
| Email search | EmailProcessing.Activities/Couriers/DPD/SearchDPDEmailsActivity.cs |
| Investigation days | EmailProcessing.Activities/Couriers/DPD/ExtractInvestigationDaysActivity.cs |
| DOR Sent | EmailProcessing.Activities/Couriers/DPD/ProcessDorSentActivity.cs |
| DOR Received | EmailProcessing.Activities/Couriers/DPD/ProcessDorReceivedActivity.cs |
| DOR Not Received | EmailProcessing.Activities/Couriers/DPD/ProcessDorNotReceivedActivity.cs |
| Unsuccessful | EmailProcessing.Activities/Couriers/DPD/ProcessUnsuccessfulClaimActivity.cs |
| All Sorted | EmailProcessing.Activities/Couriers/DPD/ProcessAllSortedActivity.cs |
| Portal | EmailProcessing.Activities/Couriers/DPD/ProcessPortalSubmissionActivity.cs |
| State model | EmailProcessing.Common/Models/DPDWorkflowModels.cs |
| Constants | Claimit.Temporal.Contracts/Constants/DPDWorkflowConstants.cs |