{
  "name": "FilePost — Create Shopify product drafts from images with OpenAI",
  "nodes": [
    {
      "parameters": {
        "content": "## Image-to-Shopify draft pipeline\n\nSend one product image URL plus price and SKU. The workflow:\n\n1. Validates and downloads the image.\n2. Hosts it with FilePost so Shopify and OpenAI receive a stable public URL.\n3. Uses the OpenAI Responses API with a real `input_image` block.\n4. Uses Shopify's current GraphQL `productSet` mutation to create a **draft** product for human review.\n\n### Setup\n- Install `n8n-nodes-filepost` and connect FilePost.\n- On **Analyze Product Image**, add Header Auth: `Authorization: Bearer YOUR_OPENAI_API_KEY`.\n- On **Create Shopify Draft**, add Header Auth: `X-Shopify-Access-Token: YOUR_TOKEN`.\n- Set `SHOPIFY_STORE` to the shop prefix (for example, `acme`, not the full domain).\n- Set `SHOPIFY_LOCATION_ID` to the inventory location GID, such as `gid://shopify/Location/123456789`.",
        "height": 560,
        "width": 620,
        "color": 7
      },
      "id": "6fa7eaaf-44e0-477a-8c06-30fe32691116",
      "name": "Setup and safety",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [-820, -400]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "filepost-shopify-product-draft",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "7a62e132-3afb-4efd-a3d4-63aabcc7c13d",
      "name": "Receive Product Image",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-780, 100],
      "webhookId": "filepost-shopify-draft-template"
    },
    {
      "parameters": {
        "jsCode": "const body = $input.first().json.body || $input.first().json;\nif (!body.image_url) throw new Error('image_url is required');\nif (!body.price || Number(body.price) <= 0) throw new Error('price must be greater than zero');\nconst imageUrl = new URL(body.image_url);\nif (!['http:', 'https:'].includes(imageUrl.protocol)) throw new Error('image_url must use http or https');\nconst clean = (value, fallback = '') => String(value || fallback).trim().replace(/[\\r\\n]+/g, ' ').slice(0, 500);\nconst sku = clean(body.sku, `SKU-${Date.now()}`).toUpperCase().replace(/[^A-Z0-9_-]/g, '');\nreturn [{ json: {\n  imageUrl: imageUrl.toString(),\n  price: Number(body.price).toFixed(2),\n  compareAtPrice: body.compare_at_price ? Number(body.compare_at_price).toFixed(2) : '',\n  sku,\n  vendor: clean(body.vendor, 'My Store'),\n  productType: clean(body.product_type, ''),\n  titleHint: clean(body.title, ''),\n  descriptionHint: clean(body.description, ''),\n  tagsHint: Array.isArray(body.tags) ? body.tags.map((tag) => clean(tag)).filter(Boolean).slice(0, 12) : [],\n  inventoryQuantity: Math.max(0, Math.floor(Number(body.inventory_quantity ?? 1))),\n  receivedAt: new Date().toISOString()\n} }];"
      },
      "id": "0f916fe4-2130-4210-985f-5056851ef5b3",
      "name": "Validate Product Payload",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-540, 100]
    },
    {
      "parameters": {
        "url": "={{ $json.imageUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "data"
            }
          }
        }
      },
      "id": "4290aca3-fd3b-45c9-9e51-9b7bd5e2cc67",
      "name": "Download Product Image",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [-300, 100]
    },
    {
      "parameters": {
        "operation": "upload",
        "binaryPropertyName": "data",
        "expiresIn": "",
        "filenameMode": "public"
      },
      "id": "713fa135-9200-4ca8-9ef2-667750a73d44",
      "name": "Host Product Image with FilePost",
      "type": "n8n-nodes-filepost.filePost",
      "typeVersion": 1,
      "position": [-60, 100]
    },
    {
      "parameters": {
        "jsCode": "const meta = $('Validate Product Payload').first().json;\nconst hosted = $input.first().json;\nconst prompt = `Analyze this product image for an ecommerce catalog. Return JSON with title, description_html, seo_description, product_type, and tags. Use the seller hints when they are accurate. Title hint: ${meta.titleHint || 'none'}. Description hint: ${meta.descriptionHint || 'none'}. Vendor: ${meta.vendor}. Product type hint: ${meta.productType || 'none'}. Existing tags: ${meta.tagsHint.join(', ') || 'none'}.`;\nreturn [{ json: { requestBody: {\n  model: 'gpt-5.6-luna',\n  input: [{ role: 'user', content: [\n    { type: 'input_text', text: prompt },\n    { type: 'input_image', image_url: hosted.url, detail: 'high' }\n  ] }],\n  text: { format: { type: 'json_object' } }\n}, filepostUrl: hosted.url, filepostFileId: hosted.file_id } }];"
      },
      "id": "fc95b8af-9bdf-45a6-ab6b-2b91184a3caf",
      "name": "Build OpenAI Vision Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [180, 100]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/responses",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.requestBody) }}",
        "options": {}
      },
      "id": "f88fc506-8d4e-4a97-bd1b-d99210a4ab5f",
      "name": "Analyze Product Image",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [420, 100]
    },
    {
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst outputText = response.output_text || (response.output || []).flatMap((item) => item.content || []).find((part) => part.type === 'output_text')?.text;\nif (!outputText) throw new Error('OpenAI returned no output_text');\nlet ai;\ntry { ai = JSON.parse(outputText); } catch (error) { throw new Error(`OpenAI returned invalid JSON: ${error.message}`); }\nconst meta = $('Validate Product Payload').first().json;\nconst hosted = $('Build OpenAI Vision Request').first().json;\nconst title = String(ai.title || meta.titleHint || meta.sku).slice(0, 120);\nconst tags = Array.isArray(ai.tags) ? ai.tags.map(String).slice(0, 20) : meta.tagsHint;\nconst descriptionHtml = String(ai.description_html || meta.descriptionHint || '');\nconst seoDescription = String(ai.seo_description || '').slice(0, 320);\nconst productType = String(ai.product_type || meta.productType || '');\nconst locationId = String($vars.SHOPIFY_LOCATION_ID || '').trim();\nif (!locationId) throw new Error('Set the SHOPIFY_LOCATION_ID n8n variable before running this workflow');\nconst file = { originalSource: hosted.filepostUrl, alt: title, contentType: 'IMAGE' };\nconst variant = {\n  optionValues: [{ optionName: 'Title', name: 'Default Title' }],\n  price: meta.price,\n  sku: meta.sku,\n  inventoryItem: { tracked: true },\n  inventoryQuantities: [{ locationId, name: 'available', quantity: meta.inventoryQuantity }]\n};\nif (meta.compareAtPrice) variant.compareAtPrice = meta.compareAtPrice;\nconst productSet = {\n  title,\n  descriptionHtml,\n  vendor: meta.vendor,\n  productType,\n  status: 'DRAFT',\n  tags,\n  seo: seoDescription ? { title, description: seoDescription } : { title },\n  productOptions: [{ name: 'Title', values: [{ name: 'Default Title' }] }],\n  files: [file],\n  variants: [variant]\n};\nconst query = `mutation CreateFilePostProductDraft($productSet: ProductSetInput!, $synchronous: Boolean!) {\n  productSet(input: $productSet, synchronous: $synchronous) {\n    product { id title status variants(first: 1) { nodes { id sku price compareAtPrice } } media(first: 5) { nodes { id alt mediaContentType status } } }\n    userErrors { field message }\n  }\n}`;\nreturn [{ json: {\n  title,\n  descriptionHtml,\n  seoDescription,\n  productType,\n  tags,\n  price: meta.price,\n  compareAtPrice: meta.compareAtPrice,\n  sku: meta.sku,\n  vendor: meta.vendor,\n  inventoryQuantity: meta.inventoryQuantity,\n  imageUrl: hosted.filepostUrl,\n  filepostFileId: hosted.filepostFileId,\n  shopifyBody: { query, variables: { productSet, synchronous: true } },\n  receivedAt: meta.receivedAt\n} }];"
      },
      "id": "60c65178-893f-4f58-a202-0d31b56d0e50",
      "name": "Prepare Shopify Draft",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [660, 100]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://{{ $vars.SHOPIFY_STORE }}.myshopify.com/admin/api/2026-07/graphql.json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.shopifyBody) }}",
        "options": {}
      },
      "id": "0d08f048-9f83-49b4-8e28-a36d781fd19d",
      "name": "Create Shopify Draft",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [900, 100]
    },
    {
      "parameters": {
        "jsCode": "const payload = $input.first().json;\nconst result = payload.data?.productSet;\nconst errors = [\n  ...(payload.errors || []).map((item) => item.message),\n  ...(result?.userErrors || []).map((item) => `${(item.field || []).join('.')}: ${item.message}`)\n].filter(Boolean);\nif (errors.length || !result?.product) throw new Error(`Shopify could not create the draft: ${errors.join('; ') || 'no product returned'}`);\nconst shopify = result.product;\nconst draft = $('Prepare Shopify Draft').first().json;\nconst numericId = String(shopify.id).split('/').pop();\nreturn [{ json: {\n  success: true,\n  status: String(shopify.status || 'DRAFT').toLowerCase(),\n  shopify_product_id: shopify.id,\n  shopify_variant_id: shopify.variants?.nodes?.[0]?.id || null,\n  title: shopify.title || draft.title,\n  sku: shopify.variants?.nodes?.[0]?.sku || draft.sku,\n  image_url: draft.imageUrl,\n  filepost_file_id: draft.filepostFileId,\n  admin_url: `https://${$vars.SHOPIFY_STORE}.myshopify.com/admin/products/${numericId}`\n} }];"
      },
      "id": "4d9aad35-6bb4-463a-b195-778c118ee16b",
      "name": "Build Draft Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1140, 100]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {"responseCode": 201}
      },
      "id": "a4a82a21-0a16-4fe8-87b4-2a13de341699",
      "name": "Return Draft Result",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [1380, 100]
    }
  ],
  "connections": {
    "Receive Product Image": {"main": [[{"node": "Validate Product Payload", "type": "main", "index": 0}]]},
    "Validate Product Payload": {"main": [[{"node": "Download Product Image", "type": "main", "index": 0}]]},
    "Download Product Image": {"main": [[{"node": "Host Product Image with FilePost", "type": "main", "index": 0}]]},
    "Host Product Image with FilePost": {"main": [[{"node": "Build OpenAI Vision Request", "type": "main", "index": 0}]]},
    "Build OpenAI Vision Request": {"main": [[{"node": "Analyze Product Image", "type": "main", "index": 0}]]},
    "Analyze Product Image": {"main": [[{"node": "Prepare Shopify Draft", "type": "main", "index": 0}]]},
    "Prepare Shopify Draft": {"main": [[{"node": "Create Shopify Draft", "type": "main", "index": 0}]]},
    "Create Shopify Draft": {"main": [[{"node": "Build Draft Result", "type": "main", "index": 0}]]},
    "Build Draft Result": {"main": [[{"node": "Return Draft Result", "type": "main", "index": 0}]]}
  },
  "active": false,
  "settings": {"executionOrder": "v1"},
  "versionId": "a6176769-0d6d-445f-8b49-b9794ccbd7ce",
  "meta": {"templateCredsSetupCompleted": false},
  "tags": []
}
