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
3PL / Warehouse Coordination Automation
[YourCompany.com] · Fulfilment Department · Prepared by FullSpec · [Today's Date]
This document is the primary technical reference for the FullSpec team building the 3PL / Warehouse Coordination automation. It covers the current-state process map with bottleneck identification, full agent specifications with IO contracts, the end-to-end data flow trace, and the recommended build stack. All build decisions, credential requirements, and implementation constraints the FullSpec team must resolve before or during the build sprint are called out explicitly in each section.
01Process snapshot
02Agent specifications
This agent is the entry point of the automation. It connects to the ShipBob API on both a daily schedule and an event-driven webhook trigger, pulls current inventory levels, inbound receipt statuses, and outbound dispatch records, then normalises all SKU references against the internal SKU mapping table before passing a clean, structured dataset downstream to the Reconciliation and Discrepancy Agent. It does not write to any system; its sole responsibility is clean data delivery. Estimated build time: 10 hours. Complexity: Moderate.
// Input
trigger_type: 'schedule' | 'webhook'
webhook_event?: 'inventory_updated' | 'receiving_order_created' | 'outbound_order_fulfilled'
webhook_payload?: ShipBobWebhookPayload
schedule_date: ISO8601 date string
// ShipBob API calls made by this agent
GET /v2/inventory -> raw_inventory_lines[]
GET /v2/receiving-orders -> raw_asn_lines[]
GET /v2/orders?status=Fulfilled -> raw_outbound_lines[]
// SKU normalisation (mapping table lookup)
shipbob_sku -> internal_sku (via Google Sheets SKU mapping table)
shipbob_location -> warehouse_code
shipbob_quantity -> quantity_on_hand (integer)
shipbob_asn_status -> asn_status: 'confirmed' | 'pending' | 'missing'
// Output
normalised_dataset: {
run_id: UUID,
run_timestamp: ISO8601,
trigger_type: string,
inventory_lines: [
{
internal_sku: string,
shipbob_sku: string,
warehouse_code: string,
quantity_on_hand: integer,
asn_status: string,
asn_id?: string,
last_updated: ISO8601
}
],
unmapped_skus: string[] // SKUs with no mapping table entry
}- ShipBob API tier: ShipBob's REST API v2 is available on all paid merchant plans. Confirm the account has API access enabled and that an OAuth 2.0 application has been registered in the ShipBob merchant portal before build begins. The access token must be stored in the shared credential store, not hard-coded.
- Webhook registration: ShipBob webhooks must be registered via POST /v2/webhook with the automation platform's publicly accessible endpoint URL. Register all three event types: inventory_updated, receiving_order_created, outbound_order_fulfilled. Confirm the endpoint is HTTPS only.
- SKU mapping table: The normalisation step depends entirely on a maintained Google Sheets SKU mapping table (columns: shipbob_sku, internal_sku, warehouse_code, active: boolean). Priya Lal is compiling this table (milestone: May 20). Build cannot proceed to normalisation logic until the table schema is finalised and at least one full SKU set is populated.
- Unmapped SKU fallback: Any ShipBob SKU not found in the mapping table must be collected into the unmapped_skus array and surfaced in the daily Slack summary as a separate warning, not silently dropped. This prevents false-negative reconciliation results.
- Dedupe on schedule + webhook: If a webhook fires on the same day as the scheduled run, the agent must check run_id uniqueness against the reconciliation log before passing downstream, to avoid double-processing the same inventory snapshot.
- Rate limits: ShipBob API enforces 100 requests per minute per merchant token. Paginate all list endpoints with a page_size of 50 and include a 700 ms inter-request delay when fetching large inventories.
This agent receives the normalised dataset from the Data Fetch agent and performs the core reconciliation logic. It calls the Cin7 API to retrieve live inventory counts for each internal SKU, compares them line by line against the ShipBob-sourced quantities, and classifies each SKU as matched, minor variance, or major discrepancy based on configured threshold values agreed with the operations manager. It also flags any SKU with an asn_status of missing or pending beyond the agreed age threshold. Results are written as a new row to the Google Sheets reconciliation log, and the classified output is passed to the Update and Notification Agent. Estimated build time: 16 hours. Complexity: Complex.
// Input
run_id: UUID
run_timestamp: ISO8601
inventory_lines: [
{ internal_sku, shipbob_sku, warehouse_code, quantity_on_hand, asn_status, asn_id?, last_updated }
]
unmapped_skus: string[]
// Cin7 API call per SKU
GET /v1/inventory?sku={internal_sku} -> cin7_quantity_on_hand: integer
-> cin7_product_id: string
-> cin7_last_modified: ISO8601
// Classification logic
variance = shipbob_quantity_on_hand - cin7_quantity_on_hand
variance_pct = abs(variance) / cin7_quantity_on_hand
classification:
'matched' if variance_pct <= minor_threshold (default 2%)
'minor_variance' if minor_threshold < variance_pct <= major_threshold (default 10%)
'major_discrepancy' if variance_pct > major_threshold OR abs(variance) > major_unit_threshold (default 10 units)
'missing_asn' if asn_status == 'missing' OR (asn_status == 'pending' AND age > asn_age_threshold_hours, default 24)
// Output (passed to Update and Notification Agent)
reconciliation_result: {
run_id: UUID,
run_timestamp: ISO8601,
classified_lines: [
{
internal_sku: string,
shipbob_sku: string,
cin7_product_id: string,
warehouse_code: string,
shipbob_qty: integer,
cin7_qty: integer,
variance: integer,
variance_pct: float,
classification: 'matched' | 'minor_variance' | 'major_discrepancy' | 'missing_asn',
asn_id?: string,
asn_status: string
}
],
unmapped_skus: string[],
total_skus_processed: integer,
matched_count: integer,
minor_variance_count: integer,
major_discrepancy_count: integer,
missing_asn_count: integer
}
// Google Sheets append (reconciliation log)
Sheet: 'Reconciliation Log'
Row fields: run_id, run_timestamp, total_skus_processed, matched_count,
minor_variance_count, major_discrepancy_count, missing_asn_count,
unmapped_skus (comma-separated), status: 'complete' | 'has_exceptions'- Cin7 API auth: Cin7 Core uses a per-account API key passed as a header (Authorization: Basic base64(account_id:api_key)). Confirm the account tier supports API access. Cin7 Omni uses a different OAuth flow. Clarify which Cin7 variant the client uses before building the integration module.
- Threshold configuration: The values for minor_threshold (percentage), major_threshold (percentage), major_unit_threshold (units), and asn_age_threshold_hours must be confirmed with the operations manager before build. Store these as environment variables in the automation platform credential store so they can be adjusted without a rebuild.
- Cin7 pagination: The Cin7 inventory endpoint returns a maximum of 250 records per page. If the SKU count exceeds 250, implement paginated fetching with the rows and page parameters.
- Google Sheets service account: The reconciliation log Google Sheet must be shared with the service account email generated during OAuth setup. The sheet ID and tab name ('Reconciliation Log') must be stored as environment variables, not hard-coded.
- Missing ASN detection: A SKU classified as missing_asn must not be auto-corrected under any threshold. It must always route to the major discrepancy branch regardless of unit variance.
- Step 10 replacement: The finance summary row appended to Google Sheets replaces the manual email to the finance team (Step 10). A separate tab ('Finance Summary') should be maintained with one aggregated row per run for Xero cross-reference. Confirm with Sam Okafor whether a Slack DM to the finance channel is also required on top of the Sheets row.
This agent acts on the classified reconciliation results. For all SKUs classified as matched or minor variance, it applies the confirmed stock quantity directly to Cin7 via the inventory adjustment endpoint and then pushes the updated level to Shopify per product variant. For all SKUs classified as major discrepancy or missing ASN, it posts a structured alert to the designated Slack fulfilment channel instead of auto-correcting. At the end of each run it assembles and posts a daily summary message to the Slack fulfilment channel covering all SKUs processed, variances found, adjustments made, and open exceptions. Estimated build time: 12 hours. Complexity: Moderate.
// Input
reconciliation_result: { ... } // full object from Reconciliation Agent
// Branch A: auto-update (matched and minor_variance SKUs)
POST /v1/inventory/adjustments (Cin7)
body: { productId: cin7_product_id, quantity: shipbob_qty, note: 'Auto-reconciled by automation run_id {run_id} at {run_timestamp}' }
GET /admin/api/2024-01/variants.json?sku={internal_sku} (Shopify)
-> variant_id, inventory_item_id, location_id
POST /admin/api/2024-01/inventory_levels/set.json (Shopify)
body: { location_id, inventory_item_id, available: shipbob_qty }
// Branch B: exception alert (major_discrepancy and missing_asn SKUs)
POST https://slack.com/api/chat.postMessage
channel: '#fulfilment-ops'
blocks: [
{ type: 'header', text: ':rotating_light: Stock Discrepancy Alert' },
{ type: 'section', fields: [
'SKU: {internal_sku}',
'ShipBob qty: {shipbob_qty}',
'Cin7 qty: {cin7_qty}',
'Variance: {variance} units ({variance_pct}%)',
'Classification: {classification}',
'ASN ID: {asn_id | none}',
'Cin7 record: https://app.cin7.com/inventory/{cin7_product_id}'
]}
]
// Daily summary message (posted after all lines processed)
POST https://slack.com/api/chat.postMessage
channel: '#fulfilment-ops'
text: 'Daily 3PL Reconciliation Summary — {run_timestamp}'
blocks: [
'Total SKUs processed: {total_skus_processed}',
'Matched and auto-updated: {matched_count + minor_variance_count}',
'Major discrepancies (action required): {major_discrepancy_count}',
'Missing ASNs (action required): {missing_asn_count}',
'Unmapped SKUs (mapping table update required): {unmapped_skus}'
]
// Output
update_summary: {
run_id: UUID,
cin7_adjustments_applied: integer,
shopify_variants_updated: integer,
slack_alerts_sent: integer,
daily_summary_sent: boolean,
errors: [{ sku: string, system: string, error_message: string }]
}- Shopify native sync check: If Cin7 and Shopify are already connected via Cin7's native Shopify integration, pushing stock updates directly to Shopify via the Admin API from this agent will cause double-adjustment. This must be confirmed during scoping. If a native sync is active, the Shopify update branch of this agent should be disabled and Cin7 should be treated as the single source of truth, with Shopify syncing from it automatically.
- Shopify location ID: Shopify inventory is location-scoped. The relevant Shopify location_id (corresponding to the ShipBob warehouse) must be retrieved and stored as an environment variable before build. Use GET /admin/api/2024-01/locations.json to enumerate available locations during setup.
- Slack bot token: A Slack app with the chat:write and channels:read OAuth scopes must be created in the [YourCompany.com] Slack workspace. The bot must be invited to #fulfilment-ops before the integration can post. Store the bot token (xoxb-...) in the shared credential store.
- Cin7 adjustment note format: Each Cin7 inventory adjustment must include a structured note in the format 'Auto-reconciled by automation run_id {run_id} at {run_timestamp}' so manual audits can distinguish automated adjustments from human edits.
- Error handling: Any failed Cin7 or Shopify API call must be retried once after a 5-second delay. If the retry also fails, the SKU must be added to the errors array, logged to the error table in Supabase, and included in the daily Slack summary as a separate 'Failed updates' line rather than silently skipped.
- Slack rate limit: The Slack Web API enforces a rate limit of approximately 1 message per second on chat.postMessage. If multiple discrepancy alerts are generated in a single run, introduce a 1.1-second delay between each alert post to avoid 429 errors. Bundle alerts into a single multi-section Block Kit message if more than 5 exceptions exist in one run.
03End-to-end data flow
// ============================================================
// TRIGGER
// ============================================================
TRIGGER: daily_schedule (06:00 local) OR shipbob_webhook_received
-> trigger_type: 'schedule' | 'webhook'
-> webhook_event?: 'inventory_updated' | 'receiving_order_created' | 'outbound_order_fulfilled'
-> schedule_date: ISO8601
-> run_id: UUID (generated at trigger)
// ============================================================
// AGENT 1: 3PL Data Fetch and Normalisation Agent
// ============================================================
ShipBob API calls:
GET /v2/inventory -> raw_inventory_lines[]
GET /v2/receiving-orders -> raw_asn_lines[]
GET /v2/orders?status=Fulfilled -> raw_outbound_lines[]
SKU mapping table lookup (Google Sheets):
shipbob_sku -> internal_sku
unmapped shipbob_skus -> unmapped_skus[] (surfaced in summary, not dropped)
Fields normalised per inventory line:
shipbob_sku, internal_sku, warehouse_code,
quantity_on_hand (integer), asn_status ('confirmed'|'pending'|'missing'),
asn_id (string|null), last_updated (ISO8601)
Dedupe check: if run_id already exists in reconciliation log -> abort, log warning
// HANDOFF: normalised_dataset -> Reconciliation and Discrepancy Agent
// Payload: { run_id, run_timestamp, trigger_type, inventory_lines[], unmapped_skus[] }
// ============================================================
// AGENT 2: Reconciliation and Discrepancy Agent
// ============================================================
For each inventory_line in normalised_dataset.inventory_lines:
Cin7 API call:
GET /v1/inventory?sku={internal_sku}
-> cin7_quantity_on_hand (integer)
-> cin7_product_id (string)
-> cin7_last_modified (ISO8601)
Compute:
variance = quantity_on_hand - cin7_quantity_on_hand
variance_pct = abs(variance) / cin7_quantity_on_hand
Classify:
variance_pct <= 0.02 -> 'matched'
0.02 < variance_pct <= 0.10 AND abs(variance) <= 10 -> 'minor_variance'
variance_pct > 0.10 OR abs(variance) > 10 -> 'major_discrepancy'
asn_status == 'missing' OR pending age > 24h -> 'missing_asn' (overrides all)
Google Sheets append (Reconciliation Log tab):
Fields: run_id, run_timestamp, total_skus_processed, matched_count,
minor_variance_count, major_discrepancy_count, missing_asn_count,
unmapped_skus (comma-separated), status
Google Sheets append (Finance Summary tab):
Fields: run_id, run_timestamp, total_adjustments_value, open_exceptions_count
// HANDOFF: reconciliation_result -> Update and Notification Agent
// Payload: { run_id, run_timestamp, classified_lines[], unmapped_skus[],
// total_skus_processed, matched_count, minor_variance_count,
// major_discrepancy_count, missing_asn_count }
// ============================================================
// AGENT 3: Update and Notification Agent
// ============================================================
Branch A: classification IN ('matched', 'minor_variance')
Cin7 inventory adjustment:
POST /v1/inventory/adjustments
body: { productId: cin7_product_id, quantity: shipbob_qty,
note: 'Auto-reconciled by automation run_id {run_id} at {run_timestamp}' }
-> cin7_adjustment_id (string)
Shopify variant lookup (if native sync not active):
GET /admin/api/2024-01/variants.json?sku={internal_sku}
-> variant_id, inventory_item_id, location_id
Shopify inventory level update:
POST /admin/api/2024-01/inventory_levels/set.json
body: { location_id, inventory_item_id, available: shipbob_qty }
-> updated: boolean
Branch B: classification IN ('major_discrepancy', 'missing_asn')
Slack alert per exception SKU:
POST chat.postMessage -> channel: '#fulfilment-ops'
fields: internal_sku, shipbob_qty, cin7_qty, variance,
variance_pct, classification, asn_id, cin7_product_id (link)
End of run (all branches complete):
Slack daily summary:
POST chat.postMessage -> channel: '#fulfilment-ops'
fields: run_timestamp, total_skus_processed, auto_updated_count,
major_discrepancy_count, missing_asn_count, unmapped_skus,
failed_updates (if any)
Error events (any API failure after 1 retry):
-> Supabase error log table: run_id, sku, system, error_message, timestamp
-> Included in daily Slack summary 'Failed updates' field
// ============================================================
// TERMINAL STATE
// ============================================================
Reconciliation log row: written (Google Sheets)
Finance summary row: written (Google Sheets)
Cin7 stock levels: updated for matched + minor_variance SKUs
Shopify inventory: updated for same SKUs (if native sync inactive)
Slack alerts: posted for each major_discrepancy and missing_asn SKU
Slack daily summary: posted to #fulfilment-ops
Error log: written to Supabase for any failed API calls
Human action required: Operations Coordinator reviews Slack alerts
for major discrepancies and missing ASNs04Recommended build stack
More documents for this process
Every document generated for 3PL / Warehouse Coordination.