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.
- Downloads the image file from the Shopify CDN.
- Sends the image bytes to Gemini together with the product title, type, vendor and tags so the model has context as well as pixels.
- Cleans the answer: one line, no wrapping quotes, no trailing period, hard capped at 125 characters.
- Writes it back with the
fileUpdateGraphQL mutation, which is what actually changes the alt attribute on a ShopifyMediaImage. - 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_productsandwrite_productsscopes. 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
- 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 includesadmin_graphql_api_id, which is the product gid the next node needs. - 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.stringifyrather than typing raw JSON matters here, because a product title with a quote or an apostrophe in it would otherwise break the request. - 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" } } - 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 samemediaconnection with no image url. - 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 todata. The node now outputs binary rather than JSON. - 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].jsonempty. - 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 namex-goog-api-keyand 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 } }) }} - 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.
- Update Alt Text In Shopify. Another HTTP Request node, POST to the same
graphql.jsonurl 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
userErrorson 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. - 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.
- 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.stringifyand the escaping is handled for you. - Sending the ProductImage gid to fileUpdate. The mutation expects the
MediaImageid that themediaconnection returns. An id taken from a REST payload is a different object and the call fails insideuserErrors. - Forgetting
write_products. A read only token queries the media fine and then silently refuses the mutation. CheckuserErrorsbefore you assume the workflow ran. - Letting the model write a paragraph. Without
maxOutputTokensand the 125 character slice you end up with alt attributes long enough that screen readers truncate them anyway. - Splitting on
imagesinstead ofmedia. 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.
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
- n8n Shopify automation, the pillar guide covering every workflow on this site.
- Connect Shopify to n8n in 2026, the Dev Dashboard credential flow this build depends on.
- Shopify AI product description generator, the same Gemini pattern applied to product copy.
- Shopify bulk product upload from CSV, for getting the products in before this workflow describes them.
- All Shopify automation guides.