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
Delivery Exception Handling
[YourCompany.com] · Fulfilment Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team everything needed to construct, configure, and connect the Delivery Exception Handling automation end to end. It covers the current-state process with its bottlenecks, full specifications for both agents, a complete data-flow trace from carrier webhook to outcome log, and the recommended build stack with all tooling decisions. The owner side is not responsible for any of the build work described here. All build, integration, and testing tasks are carried out by the FullSpec team. Questions or clarifications during the build should go to support@gofullspec.com.
01Process snapshot
02Agent specifications
The Exception Triage Agent is the first agent in the pipeline. It receives the raw ShipStation webhook payload immediately after a carrier status change fires, extracts the carrier exception code and human-readable message, fetches the full Shopify order record using the order number in the payload, and classifies the exception into one of four categories: address issue, failed attempt, customs hold, or lost parcel. The classification drives all downstream routing: it sets the resolution_path variable, selects the correct Gmail template key, and determines the Gorgias tag and queue assignment. This agent must complete and resolve before the Resolution Agent is initialised. Estimated build time: 10 hours. Complexity: Moderate.
// Input: ShipStation webhook payload
shipstation.webhook.event : 'shipment.status_changed'
shipstation.shipment_id : string
shipstation.tracking_number : string
shipstation.carrier_code : string (e.g. 'ups', 'fedex', 'usps')
shipstation.exception_code : string (carrier-specific code)
shipstation.exception_message : string
shipstation.order_number : string
// Shopify lookup (GET /admin/api/2024-01/orders.json?name={order_number})
shopify.order.id : integer
shopify.order.name : string
shopify.order.total_price : string (decimal, USD)
shopify.order.email : string
shopify.customer.first_name : string
shopify.customer.last_name : string
shopify.shipping_address.address1: string
shopify.shipping_address.city : string
shopify.shipping_address.zip : string
shopify.shipping_address.country : string
shopify.line_items : array
// Output: structured exception record passed to Resolution Agent
exception.classification : enum('address_issue','failed_attempt','customs_hold','lost_parcel')
exception.resolution_path : enum('reship','refund','carrier_trace','address_correction')
exception.gmail_template_key : string (e.g. 'address_issue_v1')
exception.gorgias_tag : string (e.g. 'exception-address')
exception.gorgias_queue : string (e.g. 'fulfilment-exceptions')
exception.order_value_usd : float
exception.requires_approval : boolean
exception.raw_payload : object (full webhook body, stored for audit)- ShipStation webhook delivery is not guaranteed real-time for all carrier integrations. A polling fallback on a 15-minute schedule must be built for carriers that do not push status changes reliably. The fallback should query ShipStation GET /shipments with status=exception and a timestamp filter of last_checked_at.
- The exception classification logic must use a two-layer lookup: first match on exception_code against a maintained carrier-code-to-category map stored in the automation platform's credential/config store; fall back to keyword matching on exception_message if the code is unrecognised. The keyword list must be confirmed with the business before build.
- Shopify API access requires the custom app scope read_orders. Confirm the Shopify plan supports private app creation (Basic and above). Store the Admin API access token in the shared credential store, not in workflow variables.
- Dedupe check required: before processing, verify the shipment_id has not already been triaged in the current session (check against the Google Sheets log or an in-memory deduplication cache). Duplicate webhook deliveries from ShipStation are common.
- If the Shopify order lookup returns zero results for the order_number, the agent must log the failure to the error table and post a Slack alert to the configured error channel before halting. Do not proceed with a null order record.
- The classification output must be confirmed with the business before build: ensure the four categories (address issue, failed attempt, customs hold, lost parcel) match the exception codes from all carriers in their specific mix.
The Resolution Agent fires after the Exception Triage Agent has set the resolution_path and after the customer notification email has been sent via Gmail and the Gorgias ticket has been opened. It evaluates the exception classification, the order value, and any customer reply captured in the Gorgias ticket to determine whether to reship or refund. For orders above the configured approval threshold it pauses execution and posts a structured Slack message to the Fulfilment Manager channel containing the order summary and recommended action, then waits for an interactive approval or rejection response. Once approved (or if the order is below threshold), it applies the resolution in Shopify, then writes the outcome record to Google Sheets and posts a summary to the Slack fulfilment channel. Estimated build time: 14 hours. Complexity: Complex.
// Input: structured exception record from Triage Agent
exception.classification : enum (from Triage Agent output)
exception.resolution_path : enum (from Triage Agent output)
exception.order_value_usd : float
exception.requires_approval : boolean
exception.gorgias_tag : string
exception.gorgias_queue : string
shopify.order.id : integer
shopify.order.name : string
shopify.line_items : array
gorgias.ticket_id : string (created in prior step)
gmail.message_id : string (sent notification reference)
// On approval (Slack interactive callback, high-value orders only)
slack.action_id : enum('approve_resolution','reject_resolution')
slack.user_id : string (Slack user ID of approving manager)
slack.approved_action : enum('reship','refund')
slack.response_timestamp : ISO8601 string
// Output: resolution applied and outcome logged
shopify.new_order_id : integer (reship path only)
shopify.refund_id : string (refund path only)
shopify.order.note : string (resolution note appended to timeline)
gorgias.ticket.status : enum('closed','open') (closed on auto-resolve)
gorgias.ticket.internal_note : string (resolution summary)
sheets.log_row.resolution_type : enum('reship','refund','escalated','rejected')
sheets.log_row.resolved_at : ISO8601 string
sheets.log_row.approved_by : string (manager Slack display name, if applicable)
sheets.log_row.order_value_usd : float
slack.channel_summary_message_ts : string (posted message timestamp)- The order value approval threshold must be set by the business before build and stored in the automation platform's config store, not hard-coded in the workflow. The threshold field name is approval_threshold_usd. Default to $150 as a placeholder; confirm the actual value with the Fulfilment Manager before go-live.
- Slack interactive messages for the approval prompt require a Slack app with the chat:write and commands OAuth scopes plus an interactive components endpoint configured in the Slack app manifest. The callback URL must be the automation platform's webhook receiver URL. Confirm the Slack app is created and installed to the workspace before building this branch.
- Shopify reship: use POST /admin/api/2024-01/orders.json with the original order's line items and shipping address. Do not duplicate discount codes or gift cards from the original order unless explicitly confirmed by the business.
- Shopify refund: use POST /admin/api/2024-01/orders/{order_id}/refunds.json with refund_line_items matching all line items and shipping: true unless otherwise specified. Required scope: write_orders.
- Gorgias ticket creation (step executed before the Resolution Agent fires but configured within this agent's build): use POST /api/v2/tickets with channel: 'email', tags matching exception.gorgias_tag, and assignee_team matching exception.gorgias_queue. Confirm the exact team and tag names with the business's Gorgias admin before build.
- Google Sheets logging requires a pre-created sheet named 'Exception Log' with the following column headers in row 1: Exception ID, Order Number, Order Value, Classification, Resolution Type, Resolved At, Approved By, Carrier, Tracking Number, Gmail Message ID, Gorgias Ticket ID. The Sheet ID must be stored in the shared credential store.
- Fallback behaviour: if the Slack approval prompt receives no response within 4 hours, the agent must re-post the prompt once. If no response is received after a further 2 hours, the case must be logged as 'escalated, no response' in Google Sheets and a follow-up Slack message posted to the error channel. Do not apply any Shopify action without an explicit approval for high-value orders.
03End-to-end data flow
// ─────────────────────────────────────────────────────────────────
// TRIGGER: ShipStation carrier exception webhook
// ─────────────────────────────────────────────────────────────────
ShipStation.webhook POST /inbound/shipstation
payload.event = 'shipment.status_changed'
payload.shipment_id = 'SS-78234'
payload.tracking_number = '1Z999AA10123456784'
payload.carrier_code = 'ups'
payload.exception_code = 'UPS-024'
payload.exception_message = 'Undeliverable: address not found'
payload.order_number = '#5821'
// Dedupe check: compare payload.shipment_id against sheets log
// -> if duplicate: halt and log 'duplicate_ignored' to error table
// ─────────────────────────────────────────────────────────────────
// STEP 1: Shopify order lookup
// ─────────────────────────────────────────────────────────────────
Shopify GET /admin/api/2024-01/orders.json
query.name = '#5821'
query.status = 'any'
response.order.id = 4502938812
response.order.name = '#5821'
response.order.email = 'customer@example.com'
response.order.total_price = '87.50'
response.customer.first_name = 'Alex'
response.shipping_address.address1 = '12 Maple St'
response.shipping_address.city = 'Portland'
response.shipping_address.zip = '97201'
response.line_items[0].title = 'Ceramic Mug - Set of 4'
response.line_items[0].quantity = 1
response.line_items[0].price = '87.50'
// ─────────────────────────────────────────────────────────────────
// AGENT HANDOFF 1: Exception Triage Agent
// Input: ShipStation payload + Shopify order record
// ─────────────────────────────────────────────────────────────────
ExceptionTriageAgent.classify()
carrier_code_map['ups']['UPS-024'] -> 'address_issue'
exception.classification = 'address_issue'
exception.resolution_path = 'address_correction'
exception.gmail_template_key = 'address_issue_v1'
exception.gorgias_tag = 'exception-address'
exception.gorgias_queue = 'fulfilment-exceptions'
exception.order_value_usd = 87.50
exception.requires_approval = false // below threshold
exception.raw_payload = { ...full webhook body... }
// ─────────────────────────────────────────────────────────────────
// STEP 2: Gorgias ticket creation
// ─────────────────────────────────────────────────────────────────
Gorgias POST /api/v2/tickets
body.channel = 'email'
body.subject = 'Delivery Exception: #5821 - Address Issue'
body.tags = ['exception-address']
body.assignee_team = 'fulfilment-exceptions'
body.customer.email = 'customer@example.com'
response.ticket_id = 'GRG-10482'
response.ticket.status = 'open'
// ─────────────────────────────────────────────────────────────────
// STEP 3: Gmail customer notification
// ─────────────────────────────────────────────────────────────────
Gmail.send(template: 'address_issue_v1')
to = 'customer@example.com'
subject = 'Update on your order #5821'
body_vars.first_name = 'Alex'
body_vars.order_name = '#5821'
body_vars.exception_type = 'address issue'
body_vars.carrier = 'UPS'
body_vars.tracking_number = '1Z999AA10123456784'
response.message_id = '<msg-abc123@gmail.com>'
// ─────────────────────────────────────────────────────────────────
// DECISION: Order value above approval threshold?
// exception.order_value_usd (87.50) < approval_threshold_usd (150)
// -> requires_approval = false -> proceed without Slack prompt
// ─────────────────────────────────────────────────────────────────
// HIGH-VALUE PATH (order_value_usd >= approval_threshold_usd):
// Slack POST /api/chat.postMessage
// channel = 'C_FULFILMENT_MANAGER'
// blocks[].text = 'Exception on #XXXX (value: $XXX). Action: reship.'
// blocks[].action_id = 'approve_resolution' | 'reject_resolution'
// -> await Slack interactive callback (timeout: 4 hrs, retry once)
// -> slack.approved_action carried into Shopify resolution step
// ─────────────────────────────────────────────────────────────────
// AGENT HANDOFF 2: Resolution Agent
// Input: exception record + gorgias.ticket_id + gmail.message_id
// ─────────────────────────────────────────────────────────────────
ResolutionAgent.resolve()
resolution_path = 'address_correction'
// address_correction path: no immediate reship/refund;
// await corrected address from customer via Gorgias reply
// OR fall back to refund after 48h with no customer response
// ─────────────────────────────────────────────────────────────────
// STEP 4: Shopify resolution (reship example)
// ─────────────────────────────────────────────────────────────────
Shopify POST /admin/api/2024-01/orders.json
body.line_items = [ { variant_id: ..., quantity: 1 } ]
body.shipping_address = { ...corrected address... }
body.note = 'Reship: exception #GRG-10482, address corrected'
body.tags = ['reship','exception-resolved']
response.new_order.id = 4502999201
response.new_order.name = '#5822'
// ─────────────────────────────────────────────────────────────────
// STEP 5: Gorgias ticket close
// ─────────────────────────────────────────────────────────────────
Gorgias PATCH /api/v2/tickets/GRG-10482
body.status = 'closed'
body.internal_note = 'Resolved: reship order #5822 created'
// ─────────────────────────────────────────────────────────────────
// STEP 6: Google Sheets outcome log
// ─────────────────────────────────────────────────────────────────
GoogleSheets.appendRow(sheet: 'Exception Log')
row.exception_id = 'EX-20240513-001'
row.order_number = '#5821'
row.order_value_usd = 87.50
row.classification = 'address_issue'
row.resolution_type = 'reship'
row.resolved_at = '2024-05-13T09:42:00Z'
row.approved_by = 'auto'
row.carrier = 'ups'
row.tracking_number = '1Z999AA10123456784'
row.gmail_message_id = '<msg-abc123@gmail.com>'
row.gorgias_ticket_id = 'GRG-10482'
// ─────────────────────────────────────────────────────────────────
// STEP 7: Slack team notification
// ─────────────────────────────────────────────────────────────────
Slack POST /api/chat.postMessage
channel = 'C_FULFILMENT_GENERAL'
text = '[RESOLVED] Exception on #5821 (address issue).
Reship #5822 created. Ticket GRG-10482 closed.'
response.message_ts = '1715593320.000400'
// ─────────────────────────────────────────────────────────────────
// END OF FLOW
// ─────────────────────────────────────────────────────────────────04Recommended build stack
More documents for this process
Every document generated for Delivery Exception Handling.