Skip to content

Goal

Stores have hundreds of products with no copy, or placeholder text (TBD, lorem ipsum). AI can write it, but unchecked AI copy invents specs and makes claims you can't back up. Build a generator that writes in batches, checks every draft, and publishes only what a human approves.

Prerequisites

  • Modules 10 (AI generation), 15 (AI with a template fallback), 18 (structured AI output)
  • For the live part: an OpenAI key and a Shopify store (a development store is free) with an Admin API access token

Concepts

  • Batching. Pick only products that need copy (empty, under 30 words, or placeholder text like TBD or lorem ipsum) and cap each run (batch_size). Cost stays predictable and the review queue stays manageable; the rest waits for the next run.
  • Brand voice in Settings. Voice, audience and banned phrases are data, not buried in the prompt, so a client can change them.
  • Structured output + code-built HTML. The model returns fields (description, bullets, SEO title, meta description, tags). Code builds and escapes the HTML, so the model can never inject markup.
  • Guardrails. Code checks every draft: banned phrases (health, eco and guarantee claims are legal risks), numbers that don't appear in the product data (a cheap, effective hallucination check), word count, and thin product data. Anything flagged is marked needs_review.
  • Fallback. If the AI fails, a plain template built from the facts goes to review instead. The run never stops.
  • Merge before shared steps. When some items go through the AI and some don't (supplied drafts), a Merge node joins both branches so the checks and the response run once.
  • Human in the loop. Nothing is published automatically. Reviewers approve (optionally with edits) or reject; publishing counts only when Shopify returns the product.

Build it

Step 1: Triggers, Settings, Route

  • Schedule Daily 7 AM, Webhooks Generate API (POST pd/generate), Review API (GET pd/review), Decide API (POST pd/review/decide).
  • Edit Fields Settings (edit me): brand name, voice, audience, banned_phrases, max_words, min_existing_words, batch_size, max_tags, use_ai.
  • Switch Route: generate / review / decide.

Step 2: Load and pick

  1. IF Products from API? → else Shopify Product → Get Many (limit 250, On Error → Continue, Always Output Data).
  2. Code Pick Products (select.js): normalize (strip HTML, count words, options → attributes), choose products needing copy (skipping ones already in review), cap at batch_size, attach run info to the first item.

Step 3: Write and check

  1. IF Use AI? → Basic LLM Chain Write with AI (prompt in code/ai-prompt.txt) + OpenAI Chat Model + Structured Output Parser Copy Format; On Error → Continue.
  2. Merge Join Branches (append): AI output on input 1, non-AI items on input 2.
  3. Code Check Copy (check.js): AI copy, supplied draft or template; guardrails; SEO cut at a word boundary (60/155); tags merged, lowercased, deduped; escaped HTML.
  4. Code Queue for Review (queue.js): save drafts (version +1 on regenerate) unless dry run → Respond (API) or Notify Reviewer (Exercise 1).

Step 4: Review and publish

  1. Code Review Queue (review.js) → Respond.
  2. Code Apply Decision (decide.js): reject → saved with note; approve → final HTML with edits.
  3. IF Publish? → Shopify Product → Update (body_html, tags), On Error → Continue → Code Record Publish (published.js): published only if Shopify returned the product id.

Publish the workflow (static data only persists for production executions).

Test it

./test/test.sh                                              # 40 assertions, no credentials
WITH_CREDS=1 SHOPIFY_PRODUCT_ID=1234567890 ./test/test.sh   # AI copy + publish one product

Check copy you already have:

curl -X POST http://localhost:5678/webhook/pd/generate -H 'Content-Type: application/json' -d '{"dry_run":true,
  "products":[{"id":"x1","title":"Wool Throw","attributes":{"material":"Merino wool"},
  "draft":{"description":"The best ever throw, warm for 10 years.","bullets":["Merino wool"]}}]}'

Common problems

  • Good copy flagged "number not in product data": add the spec to the product's attributes (or options in Shopify); the check is deliberately strict.
  • A product never gets new copy: products with a pending or rejected draft are skipped so a reviewer's queue isn't overwritten. Approve it, or regenerate with force: true.
  • Shopify update fails: the access token needs write_products; the product id must be the numeric id.
  • Copy sounds generic: make voice concrete (sentence length, words you use and avoid) and add more facts per product.