v2.0

GoRules Version 2 is here - redesigned, now with managed cloud.GoRules Version 2 is here!

Watch the launch videoWatch

Shipping Carrier Selector

Rule-based system that selects optimal carriers by analyzing package details, service level needs, and cost factors to streamline shipping decisions.

Solution

This shipping decision system automatically identifies the most suitable carriers based on precise package dimensions, weight, destination, and delivery timeline requirements. It performs initial validation of package specifications, rejecting oversized packages or invalid service requests upfront with specific error messages.

The system applies tiered service level mapping to match customer delivery needs with appropriate carrier categories. For premium same-day delivery, it recommends specialized couriers, while offering more cost-effective options for standard shipping. It calculates both actual and volumetric weight to determine the true shipping cost based on whichever is higher, and accounts for oversized package surcharges. When multiple carriers are eligible, the system provides a ranked list with comparative pricing to help users make the optimal choice based on their priority factors.

How it works

The decision graph processes shipping requests through these sequential steps:

  1. Package Validation: Checks that all dimensions and weight are positive values, within maximum limits (70kg weight, 200cm dimensions), and verifies delivery timeline options.

  2. Service Level Determination: Maps delivery timeline requirements (same-day, express, standard) to appropriate service tiers.

  3. Carrier Eligibility: Evaluates potential carriers based on service level, destination (domestic/international), and weight restrictions.

  4. Cost Calculation: Computes volumetric weight (LxWxH/5000), determines the higher of actual vs. volumetric weight, and applies carrier-specific pricing formulas with oversized package surcharges.

  5. Carrier Ranking: Filters and sorts eligible carriers according to the user's priority factor (typically cost).

Where teams use it

  • E-commerce shipping departments
  • Logistics and fulfillment centers
  • Small business shipping operations
  • Third-party logistics providers
  • Warehouse management systems
  • Multi-carrier shipping platforms

Inside the decision model

Shipping Carrier Selector ships as a JDM decision graph with 7 nodes, 3 decision tables and 16 rules. Download it, load it into GoRules, and run it as-is on Zen Engine.

Decision graph7 nodes · read-only
input requesttable validatePackageswitch switch1table determineServiceLeveltable determineEligibleCarriersexpression calculateCosts
01

Request

input

A shipping request arrives with the package's length, width, height, and weight alongside destination, deliveryTimeline, and priorityFactor. These seven fields drive everything downstream, from the validation limits to the per-carrier pricing formulas.

Sample requestJSON
{
  "length": 45,
  "width": 35,
  "height": 25,
  "weight": 12.5,
  "destination": "domestic",
  "deliveryTimeline": "express",
  "priorityFactor": "cost"
}
02

Validate Package

table

Validation runs on a first hit policy, so the earliest failing guard wins and writes its message to validation.error while setting validation.isValid to false. Non-positive dimensions or weight are rejected first, then weight > 70 trips 'Package exceeds maximum weight limit of 70kg', any side over 200 trips the dimension error, and a deliveryTimeline outside 'standard', 'express', and 'same-day' returns 'Invalid delivery timeline option'. The empty catch-all row at the bottom marks the request valid.

The 70kg ceiling matches the practical cutoff of parcel networks, which hand heavier pieces to freight services, and 200cm is a sensible girth-style limit for conveyor-sortable packages. Rejecting these upfront avoids a failed carrier booking later, and validating the timeline enum here means every later table can branch on it without re-checking for typos.

Decision tablefirst hit policy
InputErrorvalidation.errorIsValidvalidation.isValid
weight <= 0 or length <= 0 or width <= 0 or height <= 0'Package dimensions and weight must be positive values'false
weight > 70'Package exceeds maximum weight limit of 70kg'false
length > 200 or width > 200 or height > 200'Package exceeds maximum dimension limit of 200cm'false
deliveryTimeline != 'standard' and deliveryTimeline != 'express' and deliveryTimeline != 'same-day''Invalid delivery timeline option'false
--true
03

Switch1

switch

Routing splits on validation.isValid: requests that passed validation continue to service level mapping and pricing, while the default branch sends everything else straight to the response. That keeps invalid packages from reaching the carrier tables, so the caller gets the validation.error message without any misleading pricing attached.

Branches2 paths
validation.isValid Determine Service Level
otherwise Response
04

Determine Service Level

table

Timeline-to-tier mapping is a straight first-hit lookup: an invalid request maps to 'none', 'same-day' maps to 'premium', 'express' stays 'express', and 'standard' stays 'standard'. The invalid guard sits on top so a failed validation short-circuits the mapping before any timeline row can match.

Translating a customer-facing timeline into an internal serviceLevel is standard practice in multi-carrier shipping, because carriers are onboarded per service tier rather than per delivery promise. Keeping the mapping in its own table means new tiers or renamed timelines change one table instead of every eligibility rule.

Decision tablefirst hit policy
InputServiceLevelserviceLevel
validation.isValid == false'none'
deliveryTimeline == 'same-day''premium'
deliveryTimeline == 'express''express'
deliveryTimeline == 'standard''standard'
05

Response

output

The response returns the assembled decision to the caller: for a rejected package it carries validation.error and validation.isValid false, and for a valid one the eligible carriers with their carrierCosts. Because passThrough is enabled along the graph, the original request fields ride along, which makes the result auditable without re-querying the input.

06

Determine Eligible Carriers

table

Eligibility uses a collect hit policy, so every matching row contributes its carrier list and the results are gathered under eligibleCarriers. Rows key on serviceLevel, destination, and a weight cap: 'premium' domestic up to 15kg gets 'SpeedyExpress', 'express' domestic up to 30kg gets 'FastTrack' and 'QuickShip', international express up to 20kg gets 'GlobalExpress' and 'WorldWide', and the standard tiers list three domestic and two international options. A final row adds 'HeavyHauler' for anything between 50 and 70kg.

Per-carrier weight caps like these reflect how carrier contracts really work: premium couriers optimize for light, fast parcels, while heavy pieces need a carrier with the right handling equipment. Collecting all matches instead of picking one preserves the full option set, which the ranking step needs to compare prices.

Decision tablecollect hit policy
InputEligibleCarrierscarriers
validation.isValid == false[]
serviceLevel == 'premium' and destination == 'domestic' and weight <= 15['SpeedyExpress']
serviceLevel == 'express' and destination == 'domestic' and weight <= 30['FastTrack', 'QuickShip']
serviceLevel == 'express' and destination == 'international' and weight <= 20['GlobalExpress', 'WorldWide']
serviceLevel == 'standard' and destination == 'domestic' and weight <= 50['RegularPost', 'EcoShip', 'StandardCourier']
serviceLevel == 'standard' and destination == 'international' and weight <= 30['GlobalStandard', 'WorldWide']

+1 more row in the downloadable template

07

Calculate Costs

expression

Chargeable weight is computed here as max([weight, $.volumetricWeight]), with volumetricWeight = (length * width * height) / 5000, the standard dimensional-weight divisor used across the air-parcel industry so bulky-but-light boxes pay for the space they occupy. The baseCosts map then prices each carrier as a base fee plus a per-kg rate, from EcoShip at 8 + 1.1/kg up to HeavyHauler at 40 + 2.0/kg, adding a flat surcharge when isOversized (any side over 100cm) is true. The final carrierCosts list pairs each eligible carrier with its computed cost for the ranking step.

Expressions6 fields
eligibleCarriersflatten(map(eligibleCarriers, #.carriers))
volumetricWeight(length * width * height) / 5000
chargableWeightmax([weight, $.volumetricWeight])
isOversizedlength > 100 or width > 100 or height > 100
baseCosts{ "SpeedyExpress": 25 + ($.chargableWeight * 2.5) + ($.isOversized ? 15 : 0), "FastTrack": 18 + ($.chargableWeight * 1.8) + ($.isOversized ? 12 : 0), "QuickShip": 16 + ($.chargableWeight * 1.9) + ($.isOversized ? 10 : 0), "GlobalExpress": 35 + ($.chargableWeight * 4.2) + ($.isOversized ? 25 : 0), "WorldWide": 30 + ($.chargableWeight * 3.8) + ($.isOversized ? 20 : 0), "RegularPost": 10 + ($.chargableWeight * 1.2) + ($.isOversized ? 8 : 0), "EcoShip": 8 + ($.chargableWeight * 1.1) + ($.isOversized ? 7 : 0), "StandardCourier": 12 + ($.chargableWeight * 1.3) + ($.isOversized ? 9 : 0), "GlobalStandard": 25 + ($.chargableWeight * 2.8) + ($.isOversized ? 15 : 0), "HeavyHauler": 40 + ($.chargableWeight * 2.0) + ($.isOversized ? 30 : 0) }
carrierCostsfilter( map($.eligibleCarriers, { 'carrier': #, 'cost': $.baseCosts[#] }), #.cost < 999999 )

Make this template
your own.

Load Shipping Carrier Selector into GoRules, adjust the rules to your policy, and ship it behind your own API.