Back to Delivery Exception Handling

Developer Handover Pack

Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.

4 pagesPDF · Technical
FS-DOC-04Technical

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

Step
Name
Description
01 BOTTLENECK
Check Carrier Portals for Exceptions
Fulfilment Coordinator logs into each carrier portal and scans for failed, held, or undeliverable shipments two to three times per day. No alert system exists, so exceptions can sit undetected for hours. Time cost: 25 min per exception cycle.
02
Copy Exception Details to Spreadsheet
Coordinator copies order number, tracking number, customer name, exception type, and carrier message into a shared Google Sheet. Entirely manual and error-prone. Time cost: 10 min per exception.
03
Look Up Order in Shopify
Team member opens Shopify and searches the order to confirm delivery address, product, and customer contact details. Time cost: 5 min per exception.
04
Classify Exception Type
Coordinator reads the carrier message and manually decides whether the exception is an address issue, failed attempt, customs hold, or lost parcel. Time cost: 5 min per exception.
05
Email Customer About the Exception
A manual email is written and sent to the customer in Gmail explaining the situation and next steps. No template is consistently used. Time cost: 10 min per exception.
06
Create Support Ticket in Gorgias
A Gorgias ticket is opened manually by the Customer Support Rep, tagged with the exception type and queued for follow-up. Time cost: 8 min per exception.
07 BOTTLENECK
Contact Carrier to Arrange Resolution
For holds or lost parcels the coordinator contacts the carrier by phone or web form to initiate a trace, address correction, or redelivery. Hold times and slow web-form responses cause significant delays. Time cost: 12 min per exception (direct time only, excluding wait).
08
Decide on Reship or Refund
Coordinator or manager decides whether to reship or refund based on carrier outcome and customer response. High-value orders require manager involvement. Time cost: 5 min per exception.
09
Process Reship or Refund in Shopify
Coordinator creates a replacement order or processes the refund in Shopify and adds a resolution note to the original order. Time cost: 8 min per exception.
10
Update Spreadsheet with Resolution Outcome
Google Sheet is updated with resolution type, date, and cost. Inconsistently completed and not used for any automated reporting. Time cost: 4 min per exception.
11
Notify Team in Slack
A manual Slack message is sent to the fulfilment channel confirming resolution or escalation. Time cost: 2 min per exception.
Time cost summary: Total manual time per exception cycle is 94 minutes. At approximately 90 to 94 exceptions per month (roughly 22 per week) this equates to approximately 6 hours of hands-on staff time per week and around 26 hours per 30-day period. Steps 1, 2, 3, and 4 are replaced by the Exception Triage Agent. Steps 6, 8, 9, 10, and 11 are replaced by the Resolution Agent. Step 5 (customer email) is fully automated as part of the Resolution Agent notification branch. Step 7 (carrier contact) is eliminated because the automation resolves directly in Shopify; live carrier negotiation is not in scope. The only step that remains human is the Slack-based manager approval for high-value orders, which sits between the decision node and Shopify resolution.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Exception Triage Agent

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.

Trigger
ShipStation webhook POST received on the automation platform's inbound webhook endpoint. Event type filter: shipment.status_changed. Accepted statuses: exception, failed_attempt, held, undeliverable.
Tools
ShipStation (webhook source), Shopify (Admin REST API v2024-01, order lookup)
Replaces steps
Steps 1, 2, 3, and 4 (portal polling, spreadsheet logging, Shopify order lookup, manual classification)
Estimated build
10 hours / 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.
Resolution Agent

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.

Trigger
Fires on completion of the customer notification step. Receives the structured exception record output by the Exception Triage Agent plus the Gorgias ticket ID and Gmail message ID as context.
Tools
Shopify (Admin REST API v2024-01, order create and refund endpoints), Gorgias (REST API v2, ticket update and note endpoints), Slack (Web API, chat.postMessage and interactive message callbacks)
Replaces steps
Steps 6, 8, 9, 10, and 11 (Gorgias ticket creation, reship/refund decision, Shopify resolution, spreadsheet update, Slack notification)
Estimated build
14 hours / 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.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full data flow: carrier webhook trigger to Google Sheets outcome log
// ─────────────────────────────────────────────────────────────────
// 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
// ─────────────────────────────────────────────────────────────────
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation tool configured with one workflow per agent plus one shared utility workflow for the Google Sheets logging step and one for the Slack notification step. All credentials are stored in a shared credential store accessed by name reference across all workflows. No credentials are stored inline in workflow nodes.
Webhook configuration
One inbound webhook endpoint registered in the automation platform and provided to ShipStation as the webhook target URL. ShipStation webhook configured for the event type shipment.status_changed with a secret header (X-ShipStation-Signature) validated on receipt. Endpoint must respond with HTTP 200 within 5 seconds to prevent ShipStation retry flooding. A secondary polling workflow runs on a 15-minute cron schedule as a fallback, querying ShipStation GET /shipments with status=exception and a last_checked_at timestamp filter stored in the platform's static data store.
Templating approach
Four Gmail templates are maintained as plain-text and HTML pairs in the automation platform's template store, keyed by the exception.gmail_template_key field: address_issue_v1, failed_attempt_v1, customs_hold_v1, lost_parcel_v1. Template variables use double-brace syntax (e.g. {{first_name}}, {{order_name}}). Templates are populated at send time by the Resolution Agent using the Shopify order fields and exception record. Template content is owned by the business and must be confirmed and approved before go-live.
Error logging
All agent-level errors (Shopify lookup failures, Gorgias API errors, Slack delivery failures, duplicate webhook events) are written to an automation_errors table in a Supabase project. Each row captures: workflow_name, step_name, error_code, error_message, payload_snapshot (JSON), and created_at. A Slack alert is posted to the designated error channel (to be confirmed with the business) whenever a row is inserted. The Supabase project URL and service role key are stored in the shared credential store.
Testing approach
All agent builds are tested in a sandbox environment first. Shopify: use a development store with test orders. Gorgias: use the Gorgias sandbox or a staging account. Gmail: send to an internal test address only during QA. ShipStation: replay saved webhook payloads representing all four exception types using the platform's manual trigger. Slack: use a private test channel for all approval prompt testing. Google Sheets: use a duplicate sheet named 'Exception Log - TEST' during QA. Production credentials are not used until the parallel-run phase in Week 4.
Estimated total build time
Exception Triage Agent: 10 hours. Resolution Agent: 14 hours. End-to-end integration testing (all four exception types, approval path, polling fallback, error logging): 4 hours. Total: 28 hours across a 4-week delivery schedule.
Before any build work begins, the following must be confirmed with the business: (1) the exact order value threshold for manager approval; (2) the Gorgias queue name and tag taxonomy matching the existing support workflow; (3) the carrier list and exception codes for the keyword-to-category map used by the Triage Agent; (4) the Slack channel IDs for the manager approval prompt and the team notification; and (5) the Gmail sending address and whether domain authentication (SPF, DKIM) is already in place for that address. Blockers on any of these items will delay the build. Raise them in the first week of delivery. Contact support@gofullspec.com with any pre-build questions.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Delivery Exception Handling.

Launch Plan
Operations · Owner
View
ROI and Business Case
Finance · Owner
View
Process Runbook / SOP
Operations · Owner
View
Integration and API Spec
Technical · Developer
View
Test and QA Plan
Quality · Developer
View