Companion to Doormile Parcel Flow

Doormile Route Intelligence

Each leg of the journey drawn on its own — first, mid, last — with its real states, endpoints and failure branches. Then all three as one chain. Then where an AI-optimised design would actually put its intelligence, which is not where Doormile puts it now.

01First mile

Shipper to origin hub

Everything from a booking existing to a parcel being in a rider's hands. The happy path runs down the centre; every exception branch on the right is one the backend actually implements.

HAPPY PATH EXCEPTIONS Booking created Created | Pending_Pickup NATS queue → TryAssignOnce 60 s worker budget GEOSEARCH — any candidates? geoRadiusKm 10 · geoMaxCount 10 none in radius Escalated → manual assignment /admin/bookings/:id/assign-miler AI layer scores candidates routemate.workolik.com · 5 s cap timeout Greedy nearest miler deterministic fallback, never blocks commitAssignment — one transaction booking → Miler_Assigned assignment → Assigned · miler → Assigned AgentDecision row written reasoning + candidate count, for audit push notification to rider rider accepts? reject Rejected → Reassigned back into the search Accept Pickup_Scheduled · miler On_Pickup Reached doorstep Arrived_At_Pickup · arrivedat + GPS Pickup complete Picked_Up → Converted_To_Consignment COD collected · Idempotency-Key Consignment issued Collected_By_Miler · trackingno + 6-digit OTP to receiver
Figure 1 — first mileEvery branch here is real: escalation on an empty radius, the five-second greedy fallback, rejection looping back into the search, and the idempotency key that stops a retried pickup-complete collecting COD twice.

Two details worth holding onto. ALREADY_PICKED_UP and INVALID_STATE are stable machine-readable codes the rider app branches on, so the state machine is enforced server-side rather than trusted from the client. And commitAssignment writes booking, assignment and miler availability in a single transaction — there is no window where a rider is holding a job the booking does not know about.

02Mid mile

Hub to hub

The line-haul leg. This is the only one with no automation at all — every decision below is made by a person at a screen.

LINE-HAUL PATH BRANCHES Parcel arrives at origin hub Inbound barcode scan shelf assigned · Condition recorded → Inwarded_at_Hub Condition ≠ Good Damaged — held at hub exception queue, not loaded destination hub = this hub? currenthubid vs destinationhubid yes mid mile skipped straight to last-mile assignment no Tripsheet created by hand status Draft · source + destination hub Batchkind: local | transfer Items scanned onto the sheet item Pending → Loaded consignment → Tripsheet_Loaded mismatch Discrepancy scanned but not on the manifest, or missing Vehicle + driver attached status → Ready Dispatch Dispatched · dispatchtime · → In_Transit hold-vs-go decided by guess depart at 80% fill, or wait for 95%? Arrival scan & offload Arrived · arrivaltime · items Unloaded currenthubid updated final destination reached? yes → last mile no — re-inward, next leg
Figure 2 — mid mileNote the loop on the left: a parcel can ride more than one tripsheet. Nothing decides which parcels board which truck, when it should leave, or whether the lane should exist — those are the three open decisions on this leg.
The unpriced decision

Dispatch timing is the central mid-mile trade: the marginal cost of running an extra vehicle against the SLA-breach risk of the parcels you hold back. Both sides are computable — you already store sladueat on every consignment. Today it is a dispatcher's instinct.

03Last mile

Destination hub to consignee

The only leg with real road-network routing — and the only one with a retry loop, because a delivery can fail in a way a pickup cannot.

DELIVERY PATH FAILURE PATH manual assign-miler sync AI auto-assign greedy batch-assign Rider assigned assignment Assigned · miler On_Delivery SequenceMilerStops → Valhalla writes step · previouskms · cumulativeeta async, best-effort, needs ≥ 2 stops unconfigured → stops stay unsequenced assignment never fails because of this Start delivery → Out_for_Delivery rider reaches the consignee receiver present, OTP correct? OTP_REQUIRED | OTP_INVALID no Skip — failed attempt Attemptcount + 1 · reason logged re-queue for next attempt yes Deliver OTP verified · Idempotency-Key signature + photo via presigned URL DeliveryProof row written attempts exhausted RTO_Initiated Returnreason · Parentconsignmentid Returned_to_Sender the return leg is itself a full journey Delivered Codcollected reconciled Billingstatus → Billed other terminal states Missing · Damaged · Cancelled
Figure 3 — last mileAttemptcount and the skip reason are already being recorded on every failure. That is a labelled training set for first-attempt success prediction, sitting unused.
Where the money leaks

Every trip around that retry loop is a second full delivery run for one parcel. First-attempt delivery rate is the dominant cost line in Indian last-mile, and the loop above is currently entered on discovery rather than predicted in advance.

04All three

The whole flow

Every status a parcel can hold, in the order it holds them, colour-coded by leg. Read it as a snake: left to right, drop, right to left, drop, left to right.

FIRST MILE · booking Created | Pending_Pickup Miler_Assigned Pickup_Scheduled Arrived_At_Pickup Picked_Up Converted_To_ Consignment MID MILE · consignment Collected_By_ Miler Inwarded_at_Hub Tripsheet_Loaded In_Transit next hub · multi-leg hyperlocal — pincode prefix match LAST MILE Out_for_Delivery Delivered attempts exhausted RTO_Initiated Returned_to_ Sender skip → Attemptcount + 1 EXCEPTION TERMINALS · reachable from any leg Missing Damaged Cancelled
Figure 4 — the complete chainTwo entities, one chain: the first row is a PickupBooking, everything after Converted_To_Consignment is a Consignment. The teal bypass and the gold loop are the two places a parcel's path stops being linear.
LegEntityEnds whenLoopsOptimised today by
FirstPickupBookingparcel in rider's handsreject → reassignper-parcel geo-search + LLM pick
MidConsignmentat destination hubmulti-leg re-inwardnothing — hand-built tripsheets
LastConsignmentOTP verified, proof filedskip → next attemptValhalla sequencing, post-assignment
05

The ordering mistake

This is the most consequential thing in the codebase, and it is a sequencing question about the software rather than the parcels. Doormile assigns first and sequences second. Those are not two problems — they are one problem, and solving them in series throws away most of the available gain.

TODAY · ASSIGN, THEN SEQUENCE one parcel as it arrives GEOSEARCH 10 km · top 10 LLM picks one 5 s cap assignment committed Valhalla sequences that rider's stops, async can only reorder a set it did not get to choose PROPOSED · ONE JOINT SOLVE N parcels · batch window M riders · live capacity constraints slots · capacity · skills VRP solver assignment and order decided together OR-Tools / VROOM · ms complete routes rider · step · ETA LLM exceptions only overflow · absence · bad address · breakdown
Figure 5 — the fix is an ordering changeThe Valhalla client already exists and already speaks Doormile's vocabulary. Moving it in front of assignment, and feeding it the whole open set rather than one rider's inherited stops, is a smaller change than it sounds.
Why this matters more than model quality

Greedy nearest-neighbour assignment is a known-bad heuristic for vehicle routing — on realistic instances it lands well short of what a proper solver finds on the same data, and no amount of smarter per-parcel scoring recovers the gap, because the loss comes from committing to each parcel before seeing the rest. A better model choosing one rider at a time is still choosing one rider at a time.

06

Put the intelligence where it belongs

The expert move is to stop treating "AI" as one layer. Logistics decisions live on three clocks, and each clock wants a different technique. Doormile currently has a language model on the second clock, which is the one place it fits worst.

HORIZON DECIDES TECHNIQUE CADENCE Strategic the shape of the network beat & territory design · hub siting which lanes exist at all rider headcount per zone districting · facility location demand forecasting monthly Tactical the plan for this wave ← the LLM sits here today joint assignment + sequencing tripsheet load planning hold-vs-go on every departure rider-to-beat for the day MILP / VRP solver OR-Tools, VROOM, Valhalla matrix deterministic · explainable · milliseconds per wave Operational what just went wrong unresolvable address · receiver unreachable rider absent · vehicle down · overflow parcel operator asks a question in plain English LLM + rules judgment over messy, unstructured input per second Completed-route exhaust AgentDecision · breadcrumbs · arrivedat + GPS · Attemptcount · DeliveryProof geolocation → learned dwell, travel time, failure risk trains every layer
Figure 6 — three clocks, three techniquesLanguage models are strong on unstructured judgment and weak on combinatorial search. Solvers are the reverse. The current design has them swapped.

Per leg, what to build

Replace this function first

calculateETA is (distance / 20) × 60 + 10 — a flat 20 km/h against straight-line distance, plus a fixed ten-minute buffer. It sets rider expectations, customer-facing ETAs and support load. Road time from the Valhalla matrix you already pay for, multiplied by a time-of-day factor and added to learned dwell, is a same-day change with effect on every leg.

07

Build order

Sequenced by ratio of effect to effort, not by ambition. The first two need no new infrastructure at all.

#MoveLegDepends onWhy it's here
1Score AgentDecision against outcomesallnothing — data existsYou cannot currently tell whether AI dispatch beats greedy-nearest. Until you can, every other change is unmeasurable.
2Real ETA from road time + learned dwellallValhalla matrixReplaces a flat 20 km/h constant. Touches rider trust, customer ETA and support volume at once.
3Adaptive candidate setfirstnothingRemoves both failure modes of the fixed 10 km / top 10 window.
4Batched joint assign + sequencefirst, lastsolver, batch windowThe ordering fix in §05. Largest routing gain available.
5Address resolution on pgvectorlastdelivered historyTurns your own proof-of-delivery coordinates into a private geocoder for your zones.
6First-attempt success modellast#5, Attemptcount historyAttacks the dominant cost line in last mile.
7Automated tripsheet load planningmid#2, volume forecastThe only leg with no optimisation at all today.
8Hold-vs-go and dynamic cut-offsmid#7, sladueatConverts a dispatcher's guess into a priced decision.
9Beat districtingfirst, last#4, a Beat entityThe strategic layer. Only worth it once daily routing is solid.
The milk-round dividend

Every item above gets cheaper under a round model rather than more expensive. When stops are stable, the heavy optimisation moves to beat-design time — monthly, offline, with as much compute as you like — and the daily solve shrinks to the deltas: today's new stops, today's skips, today's absences. Real-time combinatorial search over the whole city, every wave, is a cost you only pay because the beats do not exist yet.