Back to home

I Built a Pipeline That Turns Messy Supplier Data Into On-Brand Product Listings — Automatically

Drop a supplier CSV. Get clean, grounded, compliance-checked Shopify drafts in under 3 minutes.

8.5 / 9
Eval score
14 / 20
Products published
~2.5 min
End-to-end runtime
AWS BedrockClaude Haiku 4.5Claude Sonnet 4.6RAGGuardrailsShopifyPython
View on GitHub

The Problem

Every retailer with more than a few hundred SKUs knows this problem.

Supplier data arrives looking like this:

supplier_feed.csvcsv
tb-0092, STAINLESS STEEL WATER BOTTLE 750ML, kitchen, Silver,
stainless steel, 750ml, $24.95, , BPA free

TB0094, Linen Throw Cushion, Home, Charcoal, 100% linen,
45cm x 45cm, 34.5, 12, Cover only no insert

TB-0098, Wool Throw Blanket, HOME, Grey Marle, 80% wool 20% acrylic,
130cm x 170cm, 89.0, 22, "Made in Australia, supplier states ""hypoallergenic"""

Inconsistent SKU formats. Random capitalisation. Prices with and without dollar signs. Missing quantities. Supplier claims in quotes that you absolutely cannot repeat without verification.

Before any of this becomes a product listing, someone has to read it, normalise it, decide what's safe to publish, write copy in the brand voice, and fill out the SEO fields. That's the expensive part. Not the writing — the judgment.

What It Does

Drop a supplier CSV into an S3 bucket. Within three minutes, clean draft listings appear in Shopify — grounded only in verified product data, written in a consistent brand voice, with unverified claims filtered out before anything goes live.

  • Inconsistent formatting and casing normalisation
  • Missing prices, missing stock quantities, duplicate SKUs
  • Supplier claims that can't be published ("hypoallergenic", "100% natural fragrance")
  • Products that aren't ready to list — flagged and held for review
  • On-brand copy that follows your voice guide and learns from gold standard examples

Everything lands as a draft. A human approves before publishing. That's intentional.

Architecture

1
Amazon S3Input

Supplier CSV lands in incoming/ prefix

2
Claude Haiku 4.5Extraction

Normalises each row: fixes SKUs, strips currency, flags duplicates and unverified claims

3
Flag filterQuality gate

Skips: out_of_stock · price_missing · possible_duplicate

4
Bedrock Knowledge BaseRAG retrieval

Fetches relevant brand voice rules + gold standard examples

5
Claude Sonnet 4.6Generation

Writes title, description, bullets, SEO metadata grounded in verified data only

6
Bedrock GuardrailValidation

Output-only check — blocks health claims and invented product attributes

7
Shopify APIWrite-back

Creates product as draft — human approves before publishing

How the Prompts Work

Extraction — structured output with XML tags and JSON fencing

The extraction prompt uses a pattern that consistently produces valid, parseable JSON. XML tags separate the instructions from the data, and a fenced example makes the expected output format unambiguous.

001_Api_Requests.ipynbpython
EXTRACTION_PROMPT = """
You are a product data normalisation engine.

<instructions>
- sku: uppercase with hyphen (e.g. TB-0091)
- product_name: Title Case, preserve scent/variant names
- dimensions: if product name contains volume (350ml, 750ml),
  extract it here when dimensions is otherwise empty
- flags: array of out_of_stock, price_missing, qty_missing,
         possible_duplicate, unverified_claim
</instructions>

<example>
Input: SKU: tb-0092, Item Name: STAINLESS STEEL WATER BOTTLE 750ML...
Output:
```json
{ "sku": "TB-0092", "product_name": "Stainless Steel Water Bottle",
  "dimensions": "750 ml", "price": 24.95, "flags": ["qty_missing"] }
```
</example>

<raw_row>{row_text}</raw_row>

Respond with ```json ... ``` only.
"""

Generation — RAG-grounded retrieval

Before generating each listing, the pipeline queries the Knowledge Base. The retrieved chunks — brand voice rules and example listings — become the brand context in the generation prompt, so the model has both the product data and the style guide before it writes a single word.

001_Api_Requests.ipynbpython
def retrieve_kb_context(query):
    resp = agent_runtime.retrieve(
        knowledgeBaseId=KB_ID,
        retrievalQuery={'text': query},
        retrievalConfiguration={
            'managedSearchConfiguration': {'numberOfResults': 3}
        }
    )
    return '\n\n---\n\n'.join(
        r['content']['text'] for r in resp['retrievalResults']
    )

The Guardrail — and Why It Runs Output-Only

This was the most interesting part to get right.

The obvious approach is to attach the guardrail to the invoke_model call. The problem: our generation prompt includes the brand voice guide, which explicitly mentions claims you must neverpublish. The guardrail's topic classifier reads those phrases and fires — blocking the request before the model has written a word.

The fix: decouple generation from validation.

001_Api_Requests.ipynbpython
def generate_listing_guarded(product):
    # Step 1: generate without guardrail
    listing_text = generate(product)

    # Step 2: validate only the output
    guard = bedrock.apply_guardrail(
        guardrailIdentifier = GUARDRAIL_ID,
        guardrailVersion    = GUARDRAIL_VERSION,
        source              = 'OUTPUT',          # <- key
        content             = [{'text': {'text': listing_text}}]
    )

    if guard['action'] == 'GUARDRAIL_INTERVENED':
        return {'sku': product['sku'], 'blocked': True}

    return json.loads(listing_text)

Results

Test run: 20 products from a messy supplier CSV

14

Published to Shopify

As drafts, ready for review

5

Skipped

Out of stock (3), missing price (1), duplicate (1)

1

Blocked by guardrail

Unverified health claim

~2.5 min

Runtime

End-to-end

Evaluation: 10-product scored set

SKUProductCaseScore
TB-0091Ceramic MugNormal9/9
TB-0096Cast Iron Skillet, 26 cmSpec-heavy9/9
TB-0093Bamboo Cutting BoardLimited data8/9
TB-0099Recycled Glass VaseEco material9/9
TB-0101Organic Cotton Tea TowelsEco material8/9
TB-0098Wool Throw BlanketUnverified claim9/9
TB-0106Soy Wax Candle – SandalwoodUnverified claim9/9
TB-0092Stainless Steel Water BottleHealth claim (BPA)7/9
TB-0110Bamboo Cotton Bath TowelMissing qty8/9
TB-0103Acacia Wood Serving BoardVariable size9/9
Average8.5 / 9

Scoring axes: factual grounding (no invented claims) · brand tone (warm, direct, no superlatives) · completeness (all fields present and informative).

What Improved v1 → v2

The first eval run averaged 7.7/9. Four issues, four fixes, taking it to 8.5.

  1. 01

    Capacity missing from titles

    "Ceramic Mug 350ml" was extracted as "Ceramic Mug" with dimensions: null.

    Fix: Updated the extraction prompt to parse volume and capacity from product names.

  2. 02

    Spec-sheet bullet format

    Sparse products defaulted to "Material: Bamboo" / "Category: Kitchen" labels.

    Fix: Added explicit WRONG/RIGHT examples to the generation prompt and gold standard docs.

  3. 03

    Scent names dropped

    "Soy Wax Candle - Sandalwood" became "Soy Wax Candle" during extraction.

    Fix: Updated the extraction prompt to preserve scent and variant names.

  4. 04

    Minor hallucination

    "Gets softer with every wash" appeared in a listing — not in the supplier data.

    Fix: Added a "Do not add general category knowledge" rule to the generation instructions.

Open Questions

BPA-free (TB-0092)

Blocked by the guardrail in one run and passed in another. The topic classifier is non-deterministic here — "BPA free" sits between a health claim and a factual material descriptor. A policy decision, not a technical one.

TB-0110 Bamboo Cotton Bath Towel

Scored 8/9 due to thin bullets. The supplier provided minimal data. The model did the right thing by not inventing care instructions — the real fix is upstream: ask suppliers for more data.

What's Left for Production

This is a personal reference implementation — not a production system. The gap is about 3.5 days of work:

  • Lambda + S3 trigger — replace the notebook with an event-driven function
  • Idempotency — check if a SKU exists in Shopify before creating
  • AWS Secrets Manager — move credentials out of .env
  • IAM role — replace long-lived access keys with an execution role
  • Guardrail versioning — promote from DRAFT to a numbered version
  • Run-level audit log — write a JSON summary per run to S3
  • Error alerting — CloudWatch + SNS on failure or block-rate spike