$npx -y skills add 40RTY-ai/shopify-admin-skills --skill shopify-admin-order-hold-and-releasePlace or release fulfillment holds on open orders in batch — with a stated reason and optional expiry date.
| 1 | ## Purpose |
| 2 | Places or releases holds on fulfillment orders programmatically without navigating the Shopify admin. Useful for fraud review queues, inventory shortages, or payment verification workflows. Works on orders with fulfillment orders in `OPEN` status. |
| 3 | |
| 4 | ## Prerequisites |
| 5 | - `shopify auth login --store <domain>` |
| 6 | - API scopes: `read_orders`, `write_merchant_managed_fulfillment_orders` |
| 7 | |
| 8 | ## Parameters |
| 9 | Universal (store, format, dry_run) + skill-specific: |
| 10 | |
| 11 | | Parameter | Type | Required | Default | Description | |
| 12 | |-----------|------|----------|---------|-------------| |
| 13 | | action | string | yes | — | `hold` or `release` | |
| 14 | | order_ids | array | no* | — | Array of order GIDs to target (e.g., `["gid://shopify/Order/123"]`) | |
| 15 | | query_filter | string | no* | — | Shopify order search query to select orders (e.g., `"tag:fraud-review"`) | |
| 16 | | reason | string | no | `OTHER` | Hold reason: `AWAITING_PAYMENT`, `HIGH_RISK_OF_FRAUD`, `INCORRECT_ADDRESS`, `INVENTORY_OUT_OF_STOCK`, `OTHER` | |
| 17 | | reason_notes | string | no | — | Free-text note visible to fulfillment staff | |
| 18 | | hold_until | string | no | — | ISO 8601 date when hold auto-expires (optional) | |
| 19 | |
| 20 | *One of `order_ids` or `query_filter` is required. |
| 21 | |
| 22 | ## Safety |
| 23 | |
| 24 | > ⚠️ Step 2 places or releases holds on live fulfillment orders. Holding an order prevents it from being fulfilled and may delay delivery. Releasing a hold allows fulfillment to proceed immediately. Run with `dry_run: true` to preview which orders will be affected before committing. |
| 25 | |
| 26 | ## Workflow Steps |
| 27 | |
| 28 | 1. **OPERATION:** `orders` — query |
| 29 | **Inputs:** `order_ids` list or `query_filter` string; fetch each order's `fulfillmentOrders` to get the fulfillment order IDs and current `status` |
| 30 | **Expected output:** List of fulfillment order GIDs with their current `status`; skip any already in the target state (already held / not held) |
| 31 | |
| 32 | 2. **OPERATION:** `fulfillmentOrderHold` — mutation (if `action: hold`) |
| 33 | **Inputs:** `id: <fulfillmentOrderId>`, `fulfillmentHold: { reason, reasonNotes, holdUntilDate }` per fulfillment order |
| 34 | **Expected output:** Updated `fulfillmentOrder.status: ON_HOLD`, `userErrors` |
| 35 | |
| 36 | **OR** |
| 37 | |
| 38 | 2. **OPERATION:** `fulfillmentOrderReleaseHold` — mutation (if `action: release`) |
| 39 | **Inputs:** `id: <fulfillmentOrderId>` per held fulfillment order |
| 40 | **Expected output:** Updated `fulfillmentOrder.status: OPEN`, `userErrors` |
| 41 | |
| 42 | ## GraphQL Operations |
| 43 | |
| 44 | ```graphql |
| 45 | # orders:query — validated against api_version 2025-01 |
| 46 | query OrdersForHold($first: Int!, $after: String, $query: String) { |
| 47 | orders(first: $first, after: $after, query: $query) { |
| 48 | edges { |
| 49 | node { |
| 50 | id |
| 51 | name |
| 52 | displayFulfillmentStatus |
| 53 | fulfillmentOrders(first: 5) { |
| 54 | edges { |
| 55 | node { |
| 56 | id |
| 57 | status |
| 58 | requestStatus |
| 59 | } |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | } |
| 64 | pageInfo { |
| 65 | hasNextPage |
| 66 | endCursor |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | ``` |
| 71 | |
| 72 | ```graphql |
| 73 | # fulfillmentOrderHold:mutation — validated against api_version 2025-01 |
| 74 | mutation FulfillmentOrderHold($id: ID!, $fulfillmentHold: FulfillmentOrderHoldInput!) { |
| 75 | fulfillmentOrderHold(id: $id, fulfillmentHold: $fulfillmentHold) { |
| 76 | fulfillmentOrder { |
| 77 | id |
| 78 | status |
| 79 | } |
| 80 | remainingFulfillmentOrder { |
| 81 | id |
| 82 | status |
| 83 | } |
| 84 | userErrors { |
| 85 | field |
| 86 | message |
| 87 | } |
| 88 | } |
| 89 | } |
| 90 | ``` |
| 91 | |
| 92 | ```graphql |
| 93 | # fulfillmentOrderReleaseHold:mutation — validated against api_version 2025-01 |
| 94 | mutation FulfillmentOrderReleaseHold($id: ID!) { |
| 95 | fulfillmentOrderReleaseHold(id: $id) { |
| 96 | fulfillmentOrder { |
| 97 | id |
| 98 | status |
| 99 | } |
| 100 | userErrors { |
| 101 | field |
| 102 | message |
| 103 | } |
| 104 | } |
| 105 | } |
| 106 | ``` |
| 107 | |
| 108 | ## Session Tracking |
| 109 | |
| 110 | **Claude MUST emit the following output at each stage. This is mandatory.** |
| 111 | |
| 112 | **On start**, emit: |
| 113 | ``` |
| 114 | ╔══════════════════════════════════════════════╗ |
| 115 | ║ SKILL: order-hold-and-release ║ |
| 116 | ║ Store: <store domain> ║ |
| 117 | ║ Started: <YYYY-MM-DD HH:MM UTC> ║ |
| 118 | ╚══════════════════════════════════════════════╝ |
| 119 | ``` |
| 120 | |
| 121 | **After each step**, emit: |
| 122 | ``` |
| 123 | [N/TOTAL] <QUERY|MUTATION> <OperationName> |
| 124 | → Params: <brief summary of key inputs> |
| 125 | → Result: <count or outcome> |
| 126 | ``` |
| 127 | |
| 128 | If `dry_run: true`, prefix every mutation step with `[DRY RUN]` and do not execute it. |
| 129 | |
| 130 | **On completion**, emit: |
| 131 | |
| 132 | For `format: human` (default): |
| 133 | ``` |
| 134 | ══════════════════════════════════════════════ |
| 135 | OUTCOME SUMMARY |
| 136 | Orders targeted: <n> |
| 137 | Fulfillment orders held/released: <n> |
| 138 | Skipped (already in target state): <n> |
| 139 | Errors: 0 |
| 140 | Output: |