Developer Handover Pack
Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.
Developer Handover Pack
Order Processing and Fulfilment
[YourCompany.com] · Fulfilment Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team every specification needed to construct, connect, and validate the three-agent Order Processing and Fulfilment automation. It covers the current-state step map with identified bottlenecks, full agent specifications with IO contracts, the end-to-end data flow trace, and the recommended build stack. The owner-side team does not need to act on anything in this document; it is written for the engineers handling Make scenario construction, API credential configuration, and agent sequencing.
01Process snapshot
02Agent specifications
Monitors the Shopify paid-order webhook stream continuously. On receipt of each new webhook payload, it queries Linnworks via API to check live available stock for every SKU in the order. If all line items are in stock it routes the order downstream to the Label and Despatch Agent and simultaneously triggers a zone-sorted pick list in Linnworks. If any SKU is out of stock it posts a structured exception alert to Slack for the Fulfilment Manager and halts further automated processing for that order. This is the entry point of the automation and must be resilient to duplicate webhook deliveries from Shopify, which are normal behaviour under retries.
// Input shopify.order_id : string // e.g. '5123456789' shopify.line_items[] : array .sku : string // must match Linnworks SKU exactly .quantity : integer shopify.shipping_address : object // full address block shopify.total_weight_g : integer // used downstream for carrier selection // Linnworks stock check (per SKU) linnworks.available_qty : integer // live available stock // Output (in-stock path) routed_order.order_id : string routed_order.line_items : array // confirmed in-stock items with SKU + qty routed_order.pick_list_id: string // Linnworks pick list reference routed_order.ship_addr : object routed_order.weight_g : integer // Output (exception path) slack.channel : '#fulfilment-exceptions' slack.message : string // order ID + out-of-stock SKUs + quantities
- Shopify tier requirement: any plan that enables webhook subscriptions (Basic and above). Register the webhook programmatically via the Shopify Admin API rather than the dashboard to ensure it survives store migrations.
- Dedupe guard: store each processed order_id in a Supabase seen_orders table and skip re-processing if the same ID arrives again within a 24-hour window. Shopify may fire the same webhook up to three times on retry.
- Linnworks API tier: Standard plan or above is required for the Inventory Service endpoints. Confirm active API key and location ID before build begins.
- SKU alignment: the Linnworks SKU field must exactly match the Shopify variant SKU field. Any mismatch produces a false out-of-stock result. A pre-build SKU audit is mandatory and must be signed off by Rachel Osei before this agent is built.
- Pick list sort: when writing the pick list to Linnworks, pass warehouse zone as the sort key so operative walking distance is minimised. Zone mapping must be documented by the warehouse team before build.
- Fallback behaviour: if the Linnworks API returns a timeout or 5xx error, do not route or reject the order. Park it in a Supabase pending_orders table and fire a Slack alert to '#fulfilment-exceptions' for manual action. Retry the stock check after 5 minutes up to three times.
Receives a stock-confirmed order record from the Order Routing Agent and orchestrates label creation, Shopify fulfilment write-back, and Xero cost posting. It sends the order weight, dimensions (if available), and destination to ShipStation, applies the pre-configured carrier selection ruleset to choose the correct service, and retrieves the label PDF and tracking number. The tracking number is written back to the Shopify order via the Fulfilment API, setting status to Fulfilled. The ShipStation shipment cost is then posted as a line item to the matching Xero sales order. This agent must complete its sequence atomically: if the Xero write fails, it should log the failure and retry rather than leave the Shopify order in an inconsistent fulfilled state.
// Input
routed_order.order_id : string
routed_order.line_items : array // SKU, qty, weight per item
routed_order.ship_addr : object // name, address1, city, postcode, country_code
routed_order.weight_g : integer
// ShipStation label request
shipstation.order_id : string // mapped from shopify.order_id
shipstation.service_code : string // resolved via carrier selection ruleset
shipstation.carrier_code : string // e.g. 'fedex', 'usps', 'ups'
// Output (ShipStation response)
shipstation.tracking_num : string
shipstation.label_url : string // PDF download URL
shipstation.shipment_cost: float // in USD
// Output (Shopify write-back)
shopify.fulfillment.tracking_number : string
shopify.fulfillment.tracking_company: string
shopify.fulfillment.status : 'success'
// Output (Xero posting)
xero.invoice_id : string // matched by order_id reference
xero.line_item.description: string // 'Shipping: {carrier} {service}'
xero.line_item.unit_amount: float // shipstation.shipment_cost
xero.line_item.account_code: string // pre-configured shipping expense account- ShipStation tier: the Gold plan or above is required for API-based label creation at the expected volume of approximately 110 orders/day. Confirm carrier accounts are connected and test label generation in ShipStation sandbox before build.
- Carrier selection ruleset: carrier_code and service_code must be resolved from a lookup table maintained in Make as a data store. Initial rule set covers: weight under 1 kg uses USPS First Class; 1 to 5 kg uses USPS Priority; over 5 kg or international uses FedEx Ground. Tom Briggs must confirm and sign off the full ruleset before this agent is built.
- Shopify Fulfillment API: use the POST /orders/{order_id}/fulfillments endpoint with notify_customer set to false, as the Customer Notification Agent handles all customer-facing communication separately.
- Xero matching: the Xero sales order is looked up by a custom order reference field populated at the time Shopify syncs to Xero. If no matching Xero invoice is found, log the mismatch to Supabase and alert Priya Nair via Slack rather than silently skipping the cost entry.
- Atomic error handling: if ShipStation returns a label successfully but the Shopify write-back fails, the tracking number must be stored in Supabase so a retry or manual paste can be performed. Never leave a label created without a corresponding Shopify fulfilment record.
- Label PDF storage: download and store the ShipStation label PDF URL in the Supabase orders table for audit purposes. Do not rely solely on ShipStation's own label archive.
Listens for the Shopify orders/fulfilled webhook event that is fired by the Label and Despatch Agent's write-back. On receipt, it identifies the tracking number, carrier name, and customer email from the Shopify fulfillment payload and fires a pre-built Klaviyo transactional event to trigger the branded tracking email. The Klaviyo event carries the tracking link, carrier display name, and an estimated delivery window calculated from the service level. This agent is the simplest of the three but is time-sensitive: the tracking email must fire within 90 seconds of the fulfilment event to meet the stated KPI.
// Input shopify.fulfillment.order_id : string shopify.fulfillment.tracking_number : string shopify.fulfillment.tracking_company : string // carrier display name shopify.fulfillment.tracking_url : string // pre-formed carrier tracking link shopify.order.email : string // customer email address shopify.order.shipping_address.name : string // customer first name for personalisation // Klaviyo event payload (POST to Track API) klaviyo.event : 'Order Shipped' klaviyo.customer_properties.$email : string klaviyo.properties.tracking_number : string klaviyo.properties.tracking_url : string klaviyo.properties.carrier_name : string klaviyo.properties.estimated_delivery_window : string // e.g. '3-5 business days' klaviyo.properties.order_id : string // Output klaviyo.event_id : string // confirmation of event receipt klaviyo.email_status : 'queued' // Klaviyo sends asynchronously
- Klaviyo plan requirement: any paid Klaviyo plan supports the server-side Track API. Confirm the private API key has 'Events: Write' scope before build. The public API key is insufficient for server-side calls.
- Email template: the 'Order Shipped' Klaviyo flow and email template must be built and approved in the Klaviyo dashboard before this agent is activated. The Make scenario only fires the event; Klaviyo owns the send logic and unsubscribe compliance.
- Estimated delivery window: calculate from a static carrier-to-days lookup stored in Make's data store (matching the same service_code values used in the Label and Despatch Agent). Do not call a live carrier ETA API at this stage; that is an Enterprise-tier enhancement.
- Deduplication: Shopify may fire multiple fulfilled webhooks for the same order if partial fulfilments are used. Check the fulfillment.id against a Supabase notified_fulfilments table before firing the Klaviyo event to prevent duplicate tracking emails.
- Fallback: if the Klaviyo Track API returns a non-200 response, log the order_id and error to Supabase and retry once after 60 seconds. If the second attempt fails, alert the fulfilment team in Slack so a manual email can be triggered.
03End-to-end data flow
// ============================================================
// ORDER PROCESSING AND FULFILMENT: END-TO-END DATA FLOW
// ============================================================
// TRIGGER
// Shopify fires orders/paid webhook on payment confirmation
SHOPIFY_WEBHOOK_PAYLOAD {
order_id : '5123456789'
line_items[] : [{ sku: 'SKU-001', quantity: 2 }, { sku: 'SKU-047', quantity: 1 }]
shipping_address : { name, address1, city, zip, country_code }
total_weight_g : 1850
customer.email : 'customer@example.com'
financial_status : 'paid'
}
// ---- AGENT 1: ORDER ROUTING AGENT -------------------------
// Step 1a: Dedupe check against Supabase seen_orders table
SUPABASE.seen_orders.SELECT(order_id) -> if found: HALT (duplicate)
// Step 1b: Per-SKU stock query to Linnworks Inventory API
LINNWORKS_REQUEST {
endpoint : GET /api/Stock/GetStockItemsFull
params : { SKUs: ['SKU-001', 'SKU-047'], location_id: '{warehouse_location_id}' }
}
LINNWORKS_RESPONSE {
'SKU-001' : { available_qty: 14 }
'SKU-047' : { available_qty: 0 } // <- out of stock
}
// Step 1c: Decision branch
IF all line_items.available_qty >= line_items.quantity
-> ROUTE TO: Label and Despatch Agent (routed_order object)
-> CREATE: Linnworks pick list (zone-sorted, pick_list_id returned)
ELSE
-> POST TO: Slack #fulfilment-exceptions
slack.message : 'Exception: Order 5123456789 | Out-of-stock: SKU-047 (need 1, have 0)'
-> WRITE TO: Supabase exceptions table
-> HALT automated flow for this order_id
// HANDOFF: Order Routing Agent -> Label and Despatch Agent
// Field: routed_order
routed_order {
order_id : '5123456789'
line_items : [{ sku: 'SKU-001', qty: 2, weight_g: 900 }] // only in-stock items
ship_addr : { name, address1, city, zip, country_code }
total_weight_g: 1850
pick_list_id : 'LW-PL-20240505-001'
}
// ---- AGENT 2: LABEL AND DESPATCH AGENT --------------------
// Step 2a: Carrier selection from Make data store ruleset
CARRIER_RULESET_LOOKUP(total_weight_g=1850) {
1000 < weight <= 5000 : carrier_code='usps', service_code='usps_priority'
}
-> resolved: carrier_code='usps', service_code='usps_priority'
// Step 2b: ShipStation label creation
SHIPSTATION_REQUEST {
endpoint : POST /orders/createshipment
body {
orderId : '5123456789'
carrierCode : 'usps'
serviceCode : 'usps_priority'
weight : { value: 1850, units: 'grams' }
shipTo : { ...ship_addr }
}
}
SHIPSTATION_RESPONSE {
shipmentId : 'SS-88291'
trackingNumber : '9400111899223456789012'
labelUrl : 'https://ssapi.shipstation.com/shipments/label/SS-88291.pdf'
shipmentCost : 7.85 // USD
carrierCode : 'usps'
}
// Step 2c: Store label URL and tracking number in Supabase orders table
SUPABASE.orders.INSERT {
order_id, tracking_number, label_url, shipment_cost, carrier_code, created_at
}
// Step 2d: Shopify fulfilment write-back
SHOPIFY_REQUEST {
endpoint : POST /orders/5123456789/fulfillments.json
body {
fulfillment {
tracking_number : '9400111899223456789012'
tracking_company : 'USPS'
tracking_url : 'https://tools.usps.com/go/TrackConfirmAction?tLabels=9400...'
notify_customer : false
}
}
}
SHOPIFY_RESPONSE {
fulfillment.id : 'FUL-774432'
fulfillment.status : 'success'
}
// Step 2e: Xero shipping cost posting
XERO_REQUEST {
endpoint : PUT /invoices/{xero_invoice_id}/lineitems
body {
description : 'Shipping: USPS Priority'
unitAmount : 7.85
accountCode : '420' // shipping expense account, confirm with Priya Nair
quantity : 1
}
}
XERO_RESPONSE {
lineItemId : 'XI-99201'
status : 'OK'
}
// HANDOFF: Label and Despatch Agent -> Customer Notification Agent
// Triggered by: Shopify orders/fulfilled webhook (auto-fired by Shopify on fulfillment write)
SHOPIFY_FULFILLED_WEBHOOK {
order_id : '5123456789'
fulfillment_id : 'FUL-774432'
tracking_number : '9400111899223456789012'
tracking_company : 'USPS'
tracking_url : 'https://tools.usps.com/go/TrackConfirmAction?tLabels=9400...'
customer.email : 'customer@example.com'
shipping_address.name: 'Jane Smith'
}
// ---- AGENT 3: CUSTOMER NOTIFICATION AGENT -----------------
// Step 3a: Dedupe check against Supabase notified_fulfilments
SUPABASE.notified_fulfilments.SELECT(fulfillment_id) -> if found: HALT
// Step 3b: Estimated delivery window lookup from Make data store
DELIVERY_WINDOW_LOOKUP(service_code='usps_priority') -> '3-5 business days'
// Step 3c: Klaviyo Track API event fire
KLAVIYO_REQUEST {
endpoint : POST https://a.klaviyo.com/api/track
body {
token : '{klaviyo_public_key}'
event : 'Order Shipped'
customer_properties {
$email : 'customer@example.com'
$first_name : 'Jane'
}
properties {
order_id : '5123456789'
tracking_number : '9400111899223456789012'
tracking_url : 'https://tools.usps.com/go/TrackConfirmAction?tLabels=9400...'
carrier_name : 'USPS Priority'
estimated_delivery_window : '3-5 business days'
}
}
}
KLAVIYO_RESPONSE {
status : 1 // 1 = accepted
}
// Step 3d: Write fulfillment_id to Supabase notified_fulfilments (dedupe record)
SUPABASE.notified_fulfilments.INSERT { fulfillment_id, order_id, sent_at }
// ============================================================
// FLOW COMPLETE
// Elapsed time trigger-to-notification: target < 90 seconds
// Manual steps remaining: Pick (Step 05), Pack (Step 06), Label apply + carrier handoff (Step 08)
// ============================================================04Recommended build stack
More documents for this process
Every document generated for Order Processing & Fulfilment.