{
  "name": "Backfill professional product photos across your Shopify catalog",
  "nodes": [
    {
      "parameters": {},
      "id": "b76639d8-ef38-4e20-a87d-e591f4595094",
      "name": "Run Product Shots",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "authentication": "accessToken",
        "resource": "product",
        "operation": "getAll",
        "returnAll": true,
        "additionalFields": {}
      },
      "id": "1c7b8f2e-b7b5-419c-bb56-ca103af04975",
      "name": "Get Products",
      "type": "n8n-nodes-base.shopify",
      "typeVersion": 1,
      "position": [
        240,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "7ef567b8-e6d4-47f6-836f-0a20ce0f4c8a",
              "leftValue": "={{ $json.tags }}",
              "operator": {
                "type": "string",
                "operation": "notContains"
              },
              "rightValue": "dreem-shots-added"
            },
            {
              "id": "c4a90d12-58be-4f21-9a3e-7d2f6b1e8a01",
              "leftValue": "={{ ($json.images ?? []).length }}",
              "operator": {
                "type": "number",
                "operation": "equals"
              },
              "rightValue": 1
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "c92f94ed-c898-4f81-a941-b8250fa51b3e",
      "name": "Missing Product Shots?",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.2,
      "position": [
        480,
        0
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "text": "=Dreem product shots run complete. Every product missing shots has been processed.",
        "otherOptions": {}
      },
      "id": "cc15ab2a-d10e-49b6-8566-39b241c5c40d",
      "name": "All Done",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.5,
      "position": [
        960,
        -160
      ]
    },
    {
      "parameters": {
        "authentication": "apiKey",
        "operation": "generateProductShots",
        "shotCodes": [
          "flat_lay_front",
          "invisible_mannequin_front"
        ],
        "frontImageUrl": "={{ $json.images[0].src }}",
        "backImageUrl": "={{ $json.images?.[1]?.src ?? '' }}",
        "outputFormat": "jpeg",
        "outputAspectRatio": 5,
        "callbackUrl": "={{ $execution.resumeUrl }}"
      },
      "id": "f8016f96-9a22-44a2-ac1e-1dc8a330f883",
      "name": "Generate Front Shots",
      "type": "n8n-nodes-dreem.dreem",
      "typeVersion": 1,
      "position": [
        960,
        128
      ]
    },
    {
      "parameters": {
        "resume": "webhook",
        "httpMethod": "POST",
        "limitWaitTime": true,
        "resumeAmount": 20,
        "resumeUnit": "minutes",
        "options": {}
      },
      "id": "8b5ac4eb-1fb2-4e54-9f7e-de9a8b5e7596",
      "name": "Wait for Render",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1184,
        128
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{ $json.body?.status ?? $json.status }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "rightValue": "completed"
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "a32b49de-2291-45d2-a693-f2db07ca7646",
      "name": "Is Complete?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1632,
        128
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "operation": "sendAndWait",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "message": "=Approve the product shots above for *{{ $('Loop Over Products').item.json.title }}*? *Approve & publish* adds them to the product; *Skip* leaves it untouched (and untagged, so it's retried next run).",
        "approvalOptions": {
          "values": {
            "approvalType": "double",
            "approveLabel": "Approve & publish",
            "disapproveLabel": "Skip"
          }
        },
        "options": {
          "limitWaitTime": {
            "values": {
              "resumeAmount": 24,
              "resumeUnit": "hours"
            }
          }
        }
      },
      "id": "77ea9023-a4eb-45b6-8106-12251e4a7eae",
      "name": "Approve in Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.5,
      "position": [
        1840,
        48
      ],
      "disabled": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{ $json.data.approved }}",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "rightValue": true
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "9e131717-e8f6-4f9c-8e94-85dcde247997",
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2064,
        48
      ],
      "disabled": true
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "64cc3d12-0826-475d-b93e-e0704dfe9077",
      "name": "Loop Over Products",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        720,
        0
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "text": "=Skipped *{{ $('Loop Over Products').item.json.title }}* - the render did not finish in time. If its shots weren't added yet, it will be picked up again on the next run.",
        "otherOptions": {}
      },
      "id": "67834686-3497-4c99-a715-4c7309c58589",
      "name": "Skipped",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.5,
      "position": [
        1840,
        240
      ]
    },
    {
      "parameters": {
        "content": "## 🟡 Setup - start here\n\nThis workflow finds products in your store that **don't have more than one product shot**, generates the rest with Dreem, and adds the images to each product - one product at a time.\n\n**How it knows:** every finished product gets the tag **`dreem-shots-added`**. Anything without that tag (and with at least one photo to work from) is picked up on the next run. Already happy with a product's shots? Add the tag manually and it will be left alone.\n\n### 1. ✏️ TO DO: add your credentials\nThe template ships without credentials. Open each node and select yours:\n- **Dreem API** → *Generate Front Shots* (+ *Generate Back Shots* if enabled)\n- **Shopify Access Token** (`read_products` + `write_products`) → *Get Products* and *Publish to Shopify* (+ *Publish Back Shots* if enabled)\n- **Slack** — a *Slack OAuth2* credential (on n8n Cloud just press *Connect*; or a *Slack API* bot token — then switch each Slack node's **Authentication** to *Access Token*) → *All Done* and *Skipped* (+ *Post Shots to Slack* and *Approve in Slack* if enabled). Pick **your** channel on each.\n\n### 2. Pick your shots\nSet **shotCodes** + **aspect ratio** on *Generate Front Shots*.\n\n### 3. Run it\nClick **Test workflow** - or swap **Run Product Shots** for a **Schedule Trigger** to run unattended.\n\n### ⚠️ Good to know\n- **One at a time** - products run sequentially; a big catalog takes a while.\n- **Needs a source photo** - only products with exactly **one** image are picked up (that image is the input). Using the back-shots row? See the **Back shots** note — it needs a 2nd image **and** a small filter change.\n- **Approve or auto** - images publish automatically by default. Enable *Approve in Slack* to review each product first.\n- **Timeouts** - renders that miss the 20-min window get a Slack notice; the product is retried on the next run.",
        "height": 780,
        "width": 980,
        "color": 7
      },
      "id": "f95144f8-95a6-44a6-a4f9-cda8f616177d",
      "name": "Setup — Start Here",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -144,
        -896
      ]
    },
    {
      "parameters": {
        "content": "### Fetch + filter\n**Get Products** pulls your whole catalog. **Missing Product Shots?** keeps only products that:\n1. don't have the `dreem-shots-added` tag yet, and\n2. have exactly **one** photo to use as input (that photo is the source).\n\nWant to exclude a product for good? Add the `dreem-shots-added` tag to it in Shopify.\n\n✏️ TO DO: select your **Shopify credential** on Get Products.",
        "height": 408,
        "width": 444,
        "color": 4
      },
      "id": "106796f1-1ffe-498e-9e99-7654018829c8",
      "name": "Note — Fetch",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -48,
        192
      ]
    },
    {
      "parameters": {
        "content": "### Loop, one product at a time\n**Loop Over Products** runs generate → wait → (optional approve) → publish per product, then moves on. Results come back via **webhook** (20-min cap). Timed-out products get a **Skipped** notice and the loop continues.\n\n✏️ TO DO: select your **Dreem credential** on Generate Front Shots, and your **Slack credential + channel** on All Done and Skipped.",
        "height": 240,
        "width": 320,
        "color": 5
      },
      "id": "35b86ff9-b207-42d9-9a5f-0bf9fe84a6d1",
      "name": "Note — Loop",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        624,
        304
      ]
    },
    {
      "parameters": {
        "content": "### Approve per product (optional)\n**Off by default** → each product's shots publish automatically.\n\n**For a human check:** enable **Post Shots to Slack**, **Approve in Slack** + **Approved?**. Each product then posts to Slack for **Approve / Skip** before publishing.\n\n✏️ TO DO if enabled: select your **Slack credential** and your **channel**.",
        "height": 390,
        "width": 312,
        "color": 3
      },
      "id": "deba1b2a-3315-486b-b12c-518e9091840c",
      "name": "Note — Approve",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1744,
        -368
      ]
    },
    {
      "parameters": {
        "content": "### Publish\nThe **Shopify** node **adds** the new shots to the product (existing photos stay) and tags it `dreem-shots-added` so it won't be processed twice.\n\n✏️ TO DO: select your **Shopify credential** here too.",
        "height": 220,
        "width": 300,
        "color": 6
      },
      "id": "4aa7ec94-e301-49e2-a1b6-3f0c6c74b717",
      "name": "Note — Publish",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2192,
        -352
      ]
    },
    {
      "parameters": {
        "authentication": "accessToken",
        "resource": "product",
        "operation": "update",
        "productId": "={{ $('Loop Over Products').item.json.id }}",
        "updateFields": {
          "images": "={{ ($('Loop Over Products').item.json.images || []).map(i => ({ id: i.id })).concat(($('Wait for Render').item.json.body.outputs || []).map(o => ({ src: o.files[0].url }))) }}",
          "tags": "={{ [$('Loop Over Products').item.json.tags, 'dreem-shots-added'].filter(Boolean).join(', ') }}"
        }
      },
      "id": "e0d5a15e-097e-4000-a4e6-2e6013991b1a",
      "name": "Publish to Shopify",
      "type": "n8n-nodes-base.shopify",
      "typeVersion": 1,
      "position": [
        2288,
        -64
      ]
    },
    {
      "parameters": {
        "authentication": "apiKey",
        "operation": "generateProductShots",
        "shotCodes": [
          "flat_lay_back",
          "invisible_mannequin_back"
        ],
        "frontImageUrl": "={{ $('Loop Over Products').item.json.images[0].src }}",
        "backImageUrl": "={{ $('Loop Over Products').item.json.images?.[1]?.src ?? '' }}",
        "outputFormat": "jpeg",
        "outputAspectRatio": 5,
        "callbackUrl": "={{ $execution.resumeUrl }}"
      },
      "id": "a28bf08d-1c63-4584-b740-5baaabb07f46",
      "name": "Generate Back Shots",
      "type": "n8n-nodes-dreem.dreem",
      "typeVersion": 1,
      "position": [
        960,
        464
      ],
      "disabled": true
    },
    {
      "parameters": {
        "resume": "webhook",
        "httpMethod": "POST",
        "limitWaitTime": true,
        "resumeAmount": 20,
        "resumeUnit": "minutes",
        "options": {}
      },
      "id": "b021109a-f98b-4082-acbf-d417c249be6d",
      "name": "Wait Back",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1184,
        464
      ],
      "disabled": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 1
          },
          "conditions": [
            {
              "leftValue": "={{ $json.body?.status ?? $json.status }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "rightValue": "completed"
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "e6f97ddc-009b-459e-a244-ce55c5a48d6e",
      "name": "Back Done?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1408,
        464
      ],
      "disabled": true
    },
    {
      "parameters": {
        "authentication": "accessToken",
        "resource": "product",
        "operation": "update",
        "productId": "={{ $('Loop Over Products').item.json.id }}",
        "updateFields": {
          "images": "={{ ($('Publish to Shopify').item.json.images || []).map(i => ({ id: i.id })).concat(($('Wait Back').item.json.body.outputs || []).map(o => ({ src: o.files[0].url }))) }}"
        }
      },
      "id": "eea42d04-6f3a-46d0-a256-51c9861a16ef",
      "name": "Publish Back Shots",
      "type": "n8n-nodes-base.shopify",
      "typeVersion": 1,
      "position": [
        1632,
        464
      ],
      "disabled": true
    },
    {
      "parameters": {
        "content": "### Back shots (optional, OFF)\n**Generate Front Shots** (ON): `flat_lay_front` + `invisible_mannequin_front`.\nWant back shots too? Enable the 4-node back row (`flat_lay_back` + `invisible_mannequin_back`). Products need a **back image as their 2nd image**.\n\nAlso widen the image filter in **Missing Product Shots?**: change **is equal to** 1 → **is less than** 3, so products with a front + back image qualify.\n\n✏️ TO DO if enabled: select **Dreem** + **Shopify credentials** on the back-row nodes.",
        "height": 368,
        "width": 444,
        "color": 6
      },
      "id": "20ae0611-0e52-442b-8704-5e92f8f87b5e",
      "name": "Note — Back Shots",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1872,
        432
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "text": "=*Product shots ready for review* — {{ $('Loop Over Products').item.json.title }}\n{{ ($('Wait for Render').item.json.body?.outputs ?? $('Wait for Render').item.json.outputs ?? []).map(o => o.files[0].url).join('\\n') }}",
        "otherOptions": {}
      },
      "id": "51f33bfe-9042-5b7b-ae28-1dc756ab30c6",
      "name": "Post Shots to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.5,
      "position": [
        1600,
        -128
      ],
      "disabled": true
    }
  ],
  "connections": {
    "Get Products": {
      "main": [
        [
          {
            "node": "Missing Product Shots?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Complete?": {
      "main": [
        [
          {
            "node": "Post Shots to Slack",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Skipped",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Approve in Slack": {
      "main": [
        [
          {
            "node": "Approved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Approved?": {
      "main": [
        [
          {
            "node": "Publish to Shopify",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over Products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Products": {
      "main": [
        [
          {
            "node": "All Done",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Generate Front Shots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skipped": {
      "main": [
        [
          {
            "node": "Loop Over Products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Render": {
      "main": [
        [
          {
            "node": "Is Complete?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Front Shots": {
      "main": [
        [
          {
            "node": "Wait for Render",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Publish to Shopify": {
      "main": [
        [
          {
            "node": "Generate Back Shots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Back Shots": {
      "main": [
        [
          {
            "node": "Wait Back",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait Back": {
      "main": [
        [
          {
            "node": "Back Done?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Back Done?": {
      "main": [
        [
          {
            "node": "Publish Back Shots",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Skipped",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Publish Back Shots": {
      "main": [
        [
          {
            "node": "Loop Over Products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Product Shots": {
      "main": [
        [
          {
            "node": "Get Products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Missing Product Shots?": {
      "main": [
        [
          {
            "node": "Loop Over Products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Shots to Slack": {
      "main": [
        [
          {
            "node": "Approve in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  }
}