HomeShopify & E-commerceShopify product image alt text generator…

Shopify product image alt text generator with n8n

Shopify product image alt text generator with n8n







A Shopify product image alt text generator in n8n closes one of the quietest gaps in a store: every photo you upload ships with an empty alt attribute, and nobody ever goes back to fill them in. This build watches for new products, sends each image without alt text to Google Gemini, and writes the returned description straight back to Shopify through the GraphQL Admin API. Eleven nodes, no theme edits, no app subscription.

What it does

The workflow fires on the Shopify products/create webhook. When a product lands, it asks Shopify for that product’s media, keeps only the images whose alt attribute is empty, and processes each one on its own.

  1. Downloads the image file from the Shopify CDN.
  2. Sends the image bytes to Gemini together with the product title, type, vendor and tags so the model has context as well as pixels.
  3. Cleans the answer: one line, no wrapping quotes, no trailing period, hard capped at 125 characters.
  4. Writes it back with the fileUpdate GraphQL mutation, which is what actually changes the alt attribute on a Shopify MediaImage.
  5. Sends you one Telegram message per product listing every alt text it just wrote, so you can spot a bad one in five seconds.
 Shopify products/create
          |
   Fetch Product Media  (GraphQL: title, type, vendor, tags, media[])
          |
    Split Out Images  -->  one item per media node
          |
   Only Images Missing Alt  (alt empty AND image url present)
          |
      Download Image  (binary from the Shopify CDN)
          |
     Image To Base64  (buffer -> base64 + product context)
          |
   Gemini Writes Alt Text  (gemini-2.5-flash, inline_data)
          |
      Build Alt Text  (trim, strip quotes, cap 125 chars)
          |
 Update Alt Text In Shopify  (fileUpdate mutation)
          |
   Collect Results  -->  Notify Telegram (one digest per product)

Why it beats the default

Shopify does not generate alt text. The admin gives you a small “Add alt text” link under each image and that is the whole feature. On a ten product drop with four photos each that is forty text boxes, so in practice it never happens and the catalog ships blind.

The apps that do this fall into two camps. Bulk SEO apps charge a monthly fee and usually template the alt text from the product title, which produces forty images all described as “Cotton Crew Neck Tee” with nothing to distinguish the back view from the folded flat lay. Vision based apps read the picture but lock you into their prompt and their credit system.

Running it in n8n gives you the vision model and the prompt. Because the image bytes go to Gemini as inline_data, the model describes what is in the frame, and because the product title, type, vendor and tags travel in the same request, it also knows the words your customers search for. You own the prompt, so if the output reads too clinical you change one sentence and rerun.

Tip: keep alt text descriptive rather than keyword stuffed. Google has said for years that alt text written for humans outperforms alt text written for crawlers, and screen reader users are the other half of the audience.

What you need

  • An n8n instance, cloud or self hosted, version 1.60 or newer.
  • A Shopify custom app with the read_products and write_products scopes. If you have not connected the two yet, follow connecting Shopify to n8n in 2026 first, since the old admin custom app screen is gone and the Dev Dashboard flow is the current one.
  • A Google AI Studio API key for Gemini. The free tier covers a normal catalog.
  • A Telegram bot and your chat id for the digest message. Optional, delete the last two nodes if you do not want it.
  • About 30 minutes to build by hand.

Node-by-node list

# Node Type Job
1 New Product Created Shopify Trigger Listens to products/create
2 Fetch Product Media HTTP Request GraphQL query for title, type, vendor, tags, media
3 Split Out Images Split Out One item per media node
4 Only Images Missing Alt Filter Keeps empty alt with a usable image url
5 Download Image HTTP Request Fetches the file as binary
6 Image To Base64 Code Buffer to base64, carries product context
7 Gemini Writes Alt Text HTTP Request Vision call to gemini-2.5-flash
8 Build Alt Text Set Cleans and caps the string
9 Update Alt Text In Shopify HTTP Request fileUpdate mutation
10 Collect Results Aggregate Rolls the per image items into one
11 Notify Telegram Telegram One digest per product

Step-by-step build

  1. New Product Created. Add a Shopify Trigger, set authentication to Access Token, pick your Shopify credential and set Topic to products/create. n8n registers the webhook with your store as soon as you activate the workflow. The payload includes admin_graphql_api_id, which is the product gid the next node needs.
  2. Fetch Product Media. Add an HTTP Request node, method POST, url https://YOUR_STORE.myshopify.com/admin/api/2026-04/graphql.json. Set Authentication to Predefined Credential Type and choose Shopify Access Token API so the header is signed for you. Set Body Content Type to JSON, switch Specify Body to Using JSON, and paste this expression:
    {{ JSON.stringify({
      query: 'query ($id: ID!) { product(id: $id) { title productType vendor tags media(first: 20) { nodes { ... on MediaImage { id alt image { url } } } } } }',
      variables: { id: $json.admin_graphql_api_id }
    }) }}

    Building the body with JSON.stringify rather than typing raw JSON matters here, because a product title with a quote or an apostrophe in it would otherwise break the request.

  3. Split Out Images. Add a Split Out node and set Field To Split Out to data.product.media.nodes. A product with four photos now produces four items.
    {
      "id": "gid://shopify/MediaImage/28374651",
      "alt": null,
      "image": { "url": "https://cdn.shopify.com/s/files/1/0742/.../tee-back.jpg" }
    }
  4. Only Images Missing Alt. Add a Filter node with two conditions joined by AND: {{ $json.alt }} is empty, and {{ $json.image.url }} is not empty. Turn on Convert Types Where Required. The first condition protects alt text you wrote by hand. The second drops videos and 3D models, which come back from the same media connection with no image url.
  5. Download Image. Add an HTTP Request node with url {{ $json.image.url }}. Open Options, add Response, set Response Format to File and Put Output In Field to data. The node now outputs binary rather than JSON.
  6. Image To Base64. Add a Code node in Run Once For All Items mode. Gemini needs base64 in the request body, and n8n binary data is not directly readable from an expression when the instance stores binaries on disk, so read it through the helper:
    const product = $('Fetch Product Media').first().json.data.product;
    const source = $('Only Images Missing Alt').all();
    const out = [];
    
    for (let i = 0; i < items.length; i++) {
      const buffer = await this.helpers.getBinaryDataBuffer(i, 'data');
      const meta = items[i].binary.data;
      out.push({
        json: {
          id: source[i].json.id,
          imageUrl: source[i].json.image.url,
          mimeType: meta.mimeType || 'image/jpeg',
          base64: buffer.toString('base64'),
          productTitle: product.title,
          productType: product.productType || '',
          vendor: product.vendor || '',
          tags: (product.tags || []).join(', '),
        },
      });
    }
    
    return out;

    The media gid is pulled back from the filter node because a file response leaves items[i].json empty.

  7. Gemini Writes Alt Text. Add an HTTP Request node, POST to https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent. Set Authentication to Generic Credential Type, then Header Auth, and create a credential with name x-goog-api-key and your AI Studio key as the value. Specify Body as JSON with this expression:
    {{ JSON.stringify({
      contents: [{ parts: [
        { text: 'You write alt text for ecommerce product photos. Describe what is visible in the image in one plain sentence under 120 characters. Name the product type, colour, material and any visible detail. Do not start with "image of" or "photo of". Do not add marketing language, brand slogans or a full stop at the end. Context from the store listing: title "' + $json.productTitle + '", type "' + $json.productType + '", vendor "' + $json.vendor + '", tags "' + $json.tags + '". Return the alt text only.' },
        { inline_data: { mime_type: $json.mimeType, data: $json.base64 } }
      ]}],
      generationConfig: { temperature: 0.2, maxOutputTokens: 120 }
    }) }}
  8. Build Alt Text. Add a Set node (Edit Fields) and turn Include Other Input Fields off. Create three string assignments:
    • mediaId = {{ $('Image To Base64').item.json.id }}
    • productTitle = {{ $('Image To Base64').item.json.productTitle }}
    • altText = {{ ($json.candidates[0].content.parts[0].text || '').replace(/[\r\n]+/g, ' ').replace(/^["']|["'.]$/g, '').trim().slice(0, 125) }}

    That last expression is the guardrail. Whatever Gemini returns becomes one line, unquoted, under 125 characters.

  9. Update Alt Text In Shopify. Another HTTP Request node, POST to the same graphql.json url with the Shopify predefined credential. Body:
    {{ JSON.stringify({
      query: 'mutation ($files: [FileUpdateInput!]!) { fileUpdate(files: $files) { files { id alt } userErrors { field message } } }',
      variables: { files: [{ id: $json.mediaId, alt: $json.altText }] }
    }) }}

    Read userErrors on your first test run. Shopify GraphQL returns HTTP 200 even when the mutation is rejected, so a scope problem shows up there and nowhere else.

  10. Collect Results and Notify Telegram. Add an Aggregate node set to Aggregate All Item Data with Output Field Name updated, then a Telegram node with your chat id and this text:
    Alt text written for {{ $json.updated.length }} image(s) on "{{ $('Build Alt Text').first().json.productTitle }}"
    
    {{ $('Build Alt Text').all().map(i => '- ' + i.json.altText).join('\n') }}

    If every image on a product already had alt text, the filter empties the branch and nothing after it runs, so you get no message at all. That is the behaviour you want.

  11. Save and activate, then create a test product with two photos. Within a few seconds the Telegram digest arrives and both images show alt text in the Shopify admin under Media.

Common mistakes

  • Reading binary data from an expression. {{ $binary.data.data }} works on an instance that keeps binaries in memory and returns a filesystem reference on one that does not. The Code node helper works in both cases, which is why it is there.
  • Writing raw JSON in the body field. Product titles contain apostrophes and quotes. Build every GraphQL body with JSON.stringify and the escaping is handled for you.
  • Sending the ProductImage gid to fileUpdate. The mutation expects the MediaImage id that the media connection returns. An id taken from a REST payload is a different object and the call fails inside userErrors.
  • Forgetting write_products. A read only token queries the media fine and then silently refuses the mutation. Check userErrors before you assume the workflow ran.
  • Letting the model write a paragraph. Without maxOutputTokens and the 125 character slice you end up with alt attributes long enough that screen readers truncate them anyway.
  • Splitting on images instead of media. The alt attribute now lives on the media object. Split the media connection and the id you carry is already the right one.

Cost at realistic volume

Assume a store that publishes 60 new products a month with an average of four photos each, so 240 images.

Item Volume Cost
Gemini 2.5 Flash vision calls 240 images, roughly 300 tokens in and 40 out each Free tier, or about $0.03 on the paid tier
Shopify Admin API 480 GraphQL calls $0, well inside the rate limit
n8n Cloud Starter 60 executions, one per product Covered by the base plan
Self hosted n8n Same $0 beyond your server
Bulk alt text app for comparison Same catalog $10 to $30 a month, forever

The interesting number is the execution count. Because the trigger fires once per product and every image is handled as an item inside that single run, a 60 product month costs 60 executions rather than 240.

Note: Gemini free tier limits are per minute as well as per day. A 40 image bulk import can trip the per minute cap, so add a Loop Over Items node with a small batch size before the Gemini call if you plan to backfill an entire catalog in one go.

Ready-to-import template

Get the Shopify Alt Text Generator template

Every step above is free to follow and the guide is complete. The download is the same workflow as a validated JSON file: import it, attach your Shopify, Gemini and Telegram credentials, change the store domain in two nodes and it runs. It skips the build, the GraphQL typing and the base64 debugging.

Download the template ($19) →

Instant download · Works on n8n Cloud and self hosted · Want it installed and tuned for your catalog? See our done for you services.

FAQ

Does alt text actually help Shopify SEO?

It helps in two places. Google Images uses alt text as a primary relevance signal for product photos, and screen readers read it aloud, which is an accessibility requirement in most markets. Neither one moves a ranking overnight, but empty alt attributes across a whole catalog cost you traffic you never see.

Can I run this over products I already published?

Yes. Swap the Shopify trigger for a Schedule Trigger and query products with a GraphQL products connection instead of a single product. Everything after Split Out Images works unchanged because it only ever sees one media node at a time.

Why does the workflow use GraphQL instead of the REST images endpoint?

Shopify has been moving product and media writes to GraphQL, and the fileUpdate mutation is the supported way to change the alt attribute on a MediaImage. Using it now means the workflow does not break the next time a REST product endpoint is retired.

What happens if Gemini returns something odd?

The Build Alt Text node strips line breaks, strips wrapping quotes and a trailing period, then cuts the string at 125 characters. A bad answer therefore becomes a short harmless sentence rather than a paragraph of markdown. You can still edit any image in the Shopify admin afterwards.

Does the workflow overwrite alt text I wrote by hand?

No. The Only Images Missing Alt filter drops every media node whose alt attribute already has a value, so anything you or a copywriter wrote survives. Only genuinely empty images reach Gemini, which also keeps the API bill down.

Related guides