Install
openclaw skills install @hussainpatan9/shopify-product-uploaderUpload and manage Shopify products individually or in bulk with SEO-optimised titles, descriptions, tags, support for variants, collections, inventory, and d...
openclaw skills install @hussainpatan9/shopify-product-uploaderUpload single products, bulk CSV batches, or image-based products to a Shopify store. Automatically generates SEO-optimised titles, descriptions, and tags in UK English. Supports variants (size, colour), collections, multi-location inventory, draft mode, archiving, duplicate SKU handling, and barcode fields.
Activate this skill when the user says any of the following (or similar):
Before running any workflow, check that the following are configured in memory. If missing, ask the user once and store in long-term memory:
SHOPIFY_STORE_HANDLE # e.g. my-store (not the full URL)
SHOPIFY_ACCESS_TOKEN # Admin API access token (starts with shpat_)
SHOPIFY_API_VERSION # Default: 2025-01 (update annually)
DEFAULT_VENDOR # Optional: store's default brand name
DEFAULT_CURRENCY # Default: GBP
DEFAULT_LANGUAGE # Default: en-GB
DEFAULT_STATUS # Default: active (options: active | draft)
SHOPIFY_LOCATION_ID # Fetched automatically on first inventory operation
Store these securely in memory under the key shopify_config.
Never log or repeat the access token back to the user.
On first use of any inventory operation, fetch and store the primary location:
GET https://{store}.myshopify.com/admin/api/{version}/locations.json
Store the first active location's id as SHOPIFY_LOCATION_ID.
If multiple locations exist, show the list and ask the user which to use as default.
Use when the user provides details for one product via text message.
Parse the user's message for:
If critical fields are missing (price, product name), ask in one message before proceeding. Do not ask for fields that can be inferred or generated (title, description, tags).
Generate the following in UK English:
Title (max 70 characters):
Description (150–300 words, HTML formatted):
<p> tags for paragraphs, <ul> / <li> for specsTags (5–10 tags):
SEO meta title (max 60 chars) and meta description (max 155 chars):
Show a structured summary to the user:
📦 Ready to upload:
Title: [generated title]
Status: [active / draft]
Price: £[price] GBP
Compare-at: £[compare_at] (if applicable)
Variants: [list]
Tags: [list]
Collection: [name or "None"]
Stock: [quantity] units
Barcode: [barcode or "—"]
Images: [count] attached / [URLs]
Description preview:
[first 100 chars of description]...
Reply YES to upload, or tell me what to change.
Do not upload until the user confirms with YES or equivalent.
POST https://{SHOPIFY_STORE_HANDLE}.myshopify.com/admin/api/{SHOPIFY_API_VERSION}/products.json
Headers:
Content-Type: application/json
X-Shopify-Access-Token: {SHOPIFY_ACCESS_TOKEN}
Request body:
{
"product": {
"title": "{generated_title}",
"body_html": "{generated_description_html}",
"vendor": "{DEFAULT_VENDOR}",
"product_type": "{inferred_product_type}",
"tags": "{comma_separated_tags}",
"status": "{active_or_draft}",
"variants": [
{
"option1": "{variant_value}",
"price": "{price}",
"compare_at_price": "{compare_at_price_or_null}",
"sku": "{sku_or_empty}",
"barcode": "{barcode_or_null}",
"inventory_management": "shopify",
"inventory_quantity": "{stock_qty}",
"fulfillment_service": "manual",
"requires_shipping": true,
"taxable": true
}
],
"options": [
{ "name": "{option_name e.g. Colour}", "values": ["{variant_values}"] }
],
"images": [
{ "src": "{image_url}" }
],
"metafields": [
{
"namespace": "global",
"key": "title_tag",
"value": "{seo_meta_title}",
"type": "single_line_text_field"
},
{
"namespace": "global",
"key": "description_tag",
"value": "{seo_meta_description}",
"type": "single_line_text_field"
}
]
}
}
Always run this step after creating a product, even for single-location stores:
POST .../inventory_levels/set.json
{
"location_id": {SHOPIFY_LOCATION_ID},
"inventory_item_id": {variant.inventory_item_id},
"available": {stock_qty}
}
This ensures inventory registers correctly on both single and multi-location stores.
On success:
✅ Product uploaded!
Title: [title]
Status: [active / draft]
Admin URL: https://{store}.myshopify.com/admin/products/{id}
Store URL: https://{store}.myshopify.com/products/{handle}
Product ID: {id}
Inventory: [qty] units at [location name]
On 422 Duplicate SKU — offer to update instead:
⚠️ SKU "[sku]" already exists:
"[existing product title]" (ID: {id})
Options:
1. Update the existing product with new data
2. Upload as new product with no SKU
3. Cancel
What would you like to do?
Use when the user shares a CSV file path or pastes CSV data.
Read the file from the provided path or parse pasted content. Accept flexible column naming — map common variants:
If column mapping is ambiguous, show detected mapping and ask user to confirm. Store confirmed mapping in memory keyed by filename pattern for future reuse.
Before showing the preview, validate:
Found 47 products in products_may.csv
Column mapping:
product_name → title ✓ | sell_price → price ✓ | rrp → compare_at_price ✓
ref → SKU ✓ | qty → inventory ✓ | img_url → image ✓ | category → collection ✓
Row Title Price Variants Images Status
1 Bamboo Cutting Board £18.99 S, M, L 1 ✓ active
2 Linen Tea Towel Set £14.50 Natural/Grey 1 ✓ active
3 Glass Cafetiere 600ml £24.99 — 1 ✓ draft
⚠️ 3 rows missing images — will upload without images
⚠️ 1 row missing price (Row 12) — will skip
🔄 2 rows have existing SKUs (Rows 8, 23) — will UPDATE those products
✅ No internal duplicate SKUs
Reply YES to proceed, or tell me to adjust specific rows.
For rows with missing or raw descriptions:
For rows that already have a full description:
Uploading... 12/47 ✅ | 2 updated 🔄 | 0 failed ❌✅ Bulk upload complete
New uploads: 42
Updated: 2
Failed: 2
Skipped: 1
Failed:
- Row 31: Invalid image URL — uploaded without image ⚠️
- Row 38: Shopify 503 timeout — retry this row
Report saved: bulk_upload_report_{timestamp}.txt
Use when the user attaches one or more product photos.
Use vision to identify:
Follow Workflow A from Step 2 onwards. If the image was attached as a file (not a URL), tell the user: "Please upload this image to Shopify Files or your CDN and share the URL — Shopify's API requires a public image URL."
GET .../custom_collections.json # list existing
POST .../collects.json # add to existing collection
{ "collect": { "product_id": {id}, "collection_id": {id} } }
POST .../custom_collections.json # create new if needed
{ "custom_collection": { "title": "{name}" } }
Triggered by: "update product", "change price", "edit listing", "add variant", "update stock"
Find product:
GET .../products.json?title={search_term}&limit=5
Common updates:
Price:
{ "product": { "variants": [{ "id": {id}, "price": "{new_price}" }] } }
Add variant:
POST .../products/{id}/variants.json
{ "variant": { "option1": "{value}", "price": "{price}", "sku": "{sku}", "barcode": "{barcode}" } }
Stock (multi-location aware):
POST .../inventory_levels/set.json
{ "location_id": {SHOPIFY_LOCATION_ID}, "inventory_item_id": {id}, "available": {qty} }
Description:
{ "product": { "body_html": "{new_html}" } }
Image:
POST .../products/{id}/images.json
{ "image": { "src": "{url}", "position": 1 } }
For "upload as draft", "don't publish yet", "save for review":
Same as Workflow A but "status": "draft". Confirmation must clearly show DRAFT status.
To publish a saved draft: "publish [product name]"
PUT .../products/{id}.json
{ "product": { "status": "active" } }
Triggered by: "take down", "archive", "unpublish", "hide from store", "remove"
Archive (reversible): Always confirm first with a warning, then:
PUT .../products/{id}.json
{ "product": { "status": "archived" } }
Permanent delete (only if user explicitly says "delete permanently" or "delete forever"):
Confirm by typing DELETE — this cannot be undone.
DELETE .../products/{id}.json
| Error | Cause | Action |
|---|---|---|
| 401 | Invalid/expired token | Ask user to regenerate in Shopify admin |
| 403 | Missing API scope | Name the exact missing scope |
| 422 Duplicate SKU | SKU exists | Offer to update existing product |
| 422 Missing field | Required field absent | Name specific field, ask user |
| 429 Rate limit | Too many requests | Auto-retry after 2s, up to 3× |
| 404 | Wrong store handle or ID | Verify handle, re-fetch product list |
| 503 | Shopify outage | Wait 30s, retry once, then report |
Store after each session: