Sellable Everywhere: A Publish-Readiness Engine for Omnichannel SKUs
Sellable Everywhere: A Publish-Readiness Engine for Omnichannel SKUs
The same sneaker is telling four different lies right now. The app shows it in stock; the website calls it sold out. The marketplace listing carries last quarter’s price. And on search it doesn’t appear at all, because the size attribute never made it into the feed — so the channel’s filter quietly hid it. One SKU, four surfaces, four broken truths. Each system is doing exactly what it was told. The customer just can’t buy the shoe anywhere it actually exists.
This is the omnichannel commerce-platform team’s daily failure mode, and it has a precise cost. A published-but-broken SKU is worse than one that isn’t published at all: a missing attribute is a lost sale you can’t see, a stale price is a margin leak or a chargeback, and an out-of-sync count is an oversell that ends in a cancellation email. “Live” is not the same as “sellable.” This playbook is the build for closing that gap — a reference architecture and the runbooks behind it for scoring every SKU’s readiness per channel, publishing the ones that clear, repairing the ones that can, and keeping inventory and price in lockstep across web, app, marketplace, and store once they go.
One SKU, many channels, many rule sets
The reason a single product fractures into four truths is that “publishable” is not a property of the SKU — it’s a property of the pair (SKU, channel). The web storefront will happily render an item with two images and a short title. The marketplace rejects it: it wants six images at a fixed aspect ratio, a GTIN, a category-specific attribute set, and a title under its own length cap. The endless-aisle kiosk ignores imagery but won’t sell an item whose tax class is unset. The app inherits the web feed but applies a stricter availability rule because it promises same-day pickup.
So an item can be perfectly valid on two channels and invalid on the other two with no change to the product record at all — the defect lives in the gap between one product and four requirement sets. Treating publish as a single boolean is what lets a SKU go live broken. The job is to replace that boolean with a per-channel readiness verdict, and to make the channel adapters consume that verdict rather than each re-deriving “is this good enough?” in its own undocumented way.
That verdict needs a home that is neither the PIM (which holds product truth but knows nothing about Amazon’s image rules) nor the channel connector (which knows the rules but must never become a second product database). It belongs in a governed layer between them.
The boundary is the whole point. The PIM, ERP, and OMS stay authoritative; the engine reads them and never corrupts them. Enrichment it generates — a derived attribute, a cropped image variant, a channel-specific title — is written to a projection the channel consumes, with a pointer back to the source, so a merchandiser can always see what the engine added and promote it into the PIM deliberately rather than discovering mystery data weeks later.
The readiness score: four dimensions, one gate per channel
Readiness isn’t a vibe; it’s a computed score with four components, evaluated against each channel’s published contract. A SKU earns a score per channel, and the score decomposes so the engine knows not just that it failed but what to do about it.
- Attribute completeness — does the SKU carry every field this channel requires for this category, in the channel’s value vocabulary? (Marketplace “Color: Heather Grey” vs. the app’s free-text “Gray” is a mapping failure, not a missing field.)
- Imagery validity — count, resolution, aspect ratio, background, and alt-text against the channel’s media spec.
- Price validity — a non-null, in-currency price that clears the channel’s floor rules, MAP/MSRP constraints, and any active promo guardrail.
- Availability validity — a stock figure the channel can actually trust, with the right buffer and the right semantics (sellable vs. on-hand vs. reserved differ by channel).
Each dimension is scored against the channel, not in the abstract, and the gate is a per-channel policy — the same SKU passes one and fails another, and the engine records which dimension killed it.
-- Per-(sku, channel) readiness, decomposed so the router knows the fix
SELECT
p.sku,
ch.channel,
-- attribute completeness: required fields present AND mapped to channel vocabulary
(COUNT(*) FILTER (WHERE rq.required AND av.value IS NOT NULL AND av.mapped))::float
/ NULLIF(COUNT(*) FILTER (WHERE rq.required), 0) AS attr_score,
-- imagery: images meeting this channel's media spec vs. minimum required
LEAST(1.0, img.valid_count::float / ch.min_images) AS image_score,
-- price: present, in-currency, clears floor / MAP for this channel
(pr.price IS NOT NULL AND pr.price >= ch.price_floor
AND pr.price >= COALESCE(pr.map_price, 0))::int AS price_ok,
-- availability: trustworthy sellable stock above this channel's buffer
(inv.sellable - ch.safety_buffer >= ch.min_publish_qty)::int AS avail_ok
FROM product p
JOIN channel ch ON ch.active
JOIN channel_required_attr rq ON rq.channel = ch.channel AND rq.category = p.category
LEFT JOIN attr_value av ON av.sku = p.sku AND av.attr = rq.attr AND av.channel = ch.channel
LEFT JOIN image_validity img ON img.sku = p.sku AND img.channel = ch.channel
LEFT JOIN price pr ON pr.sku = p.sku AND pr.channel = ch.channel
LEFT JOIN inventory_sellable inv ON inv.sku = p.sku
WHERE p.sku = :sku
GROUP BY p.sku, ch.channel, img.valid_count, ch.min_images,
pr.price, ch.price_floor, pr.map_price,
inv.sellable, ch.safety_buffer, ch.min_publish_qty;
The non-negotiable is that the channel requirement set is data, not code buried in an adapter. channel_required_attr, min_images, price_floor, and safety_buffer are versioned rows. When the marketplace changes its category schema — and it will, without asking — you update a contract, re-score, and watch which SKUs just fell out of readiness. If those rules live inside four connectors, every channel change is a code deploy and a guessing game.
Publish, enrich, or hold — routed per channel
A readiness score is only useful if it routes to an action. The engine sorts every (SKU, channel) pair into exactly three outcomes, and the split is where the automation pays for itself.
- 01Scoredecompose readiness per (sku, channel)
- 02Publishall gates clear → push to channel now
- 03Enrichrecoverable defect → auto-fix, re-score, publish
- 04Holdunrecoverable / risky → queue a human with the reason
This is where AI earns its keep, and it earns it in the enrich lane specifically — not by “writing listings” wholesale, but by repairing the recoverable middle. A missing channel-vocabulary attribute is a mapping the model infers from the source value and the category taxonomy. A title too long for the marketplace is a rewrite to the cap that keeps the keyword head. A wrong aspect ratio is a deterministic crop; an unmapped category is a classification. Each is a bounded repair with a confidence score and a re-validation pass: the engine only publishes the enriched result if it now clears the same gate the human would have to.
Authority is earned per repair class, not granted globally. A crop or a unit normalization runs unattended — cheap to reverse, easy to verify. An AI-inferred safety or compliance attribute — an allergen field, an age rating — stays in hold under human review no matter how confident the model is, because a wrong publish there isn’t a lost sale, it’s a suspended marketplace account. Gate tuning follows the cost of the mistake, not the model’s self-reported certainty: holding a good SKU costs hours of availability; publishing a bad one on a regulated attribute can cost the channel.
RULE PublishReadiness
WHEN channel == "marketplace" AND category in ["Footwear","Apparel"]
THEN require attrs ["brand","color","size","material","gtin","title<=200"]
AND require image_score == 1.0 AND price_ok AND avail_ok
WHEN channel == "web"
THEN require attrs ["brand","title","description","size"]
AND require image_count >= 2 AND price_ok
WHEN channel == "app" AND fulfillment == "same_day_pickup"
THEN require avail_ok AND store_stock_confidence >= 0.85
WHEN defect.class in ["unmapped_attr","title_over_cap","wrong_aspect_ratio","unmapped_category"]
THEN route = "Enrich" -- auto-fix, then re-score against the same gate
WHEN defect.class in ["regulated_attr","price_below_map","no_trusted_stock"]
THEN route = "Hold" -- human gate, never auto-published
DECISION:
IF all_gates_pass THEN route = "Publish"
ELSE IF recoverable THEN route = "Enrich"
ELSE route = "Hold"
Every evaluation emits one replay-safe event — the record that lets a merchandiser ask, three weeks later, why is this SKU live on web but held on the marketplace?
{
"eventType": "PublishReadinessEvaluated",
"eventVersion": "1.0",
"sku": "SKU-441992",
"channel": "marketplace_us",
"readiness": { "attr": 0.83, "image": 1.0, "price": true, "avail": true },
"failingDimension": "attr",
"missingAttrs": ["material"],
"route": "Enrich",
"enrichment": { "attr": "material", "value": "Mesh", "source": "ai_inferred", "confidence": 0.88 },
"rescoredRoute": "Publish",
"policyVersion": "retail.publish.v12",
"modelVersion": "enrich-model-3.4.1",
"correlationId": "2f17f6c2-1a3c-4d9f-a7b1-1142f81c4d01",
"evaluatedAt": "2026-02-24T15:20:12Z"
}
The discipline these snippets enforce: a correlationId that links the original score to the enrichment and the eventual channel ACK, versioned policy and enrichment models, idempotent replay so a re-scored SKU doesn’t double-publish, an eventVersion so the contract can evolve, and an operator-facing reason on every hold. Every auto-enrichment is tagged with its source and confidence so nothing the model invented is mistaken for product truth.
Keeping price and availability in sync — the second half of the problem
Getting a SKU published correctly is the easy half. The hard half is the next four weeks, because price and inventory don’t sit still. The shoe sells. The price drops in a flash sale. A return restocks one unit. The marketplace holds two for an in-flight order. If the engine publishes once and walks away, every one of those events reopens the four-broken-truths problem it just closed — the listing is correct at 9:01 and wrong by noon.
So behind the publish path sits a sync fabric: the engine subscribes to price and inventory changes from the authoritative record and fans them out to every channel where the SKU is live, each translated into that channel’s semantics. A sellable count of 7 becomes “7 minus the web buffer” on web, “available” as a boolean on a channel that doesn’t take numbers, and a same-day figure scoped to one store on the app. One source event, four channel-correct writes — and a per-channel ACK that proves the write landed.
What prevents oversell is that the sellable figure fanned out is a single derived number, not four independent reads of raw on-hand. Sellable equals on-hand minus reserved minus the channel buffer, computed once and decremented atomically as each channel commits a sale. When two channels race for the last unit, the first commit wins and the second is rejected before it confirms to the customer — oversell is prevented at the engine, not apologized for after the OMS catches up overnight.
Reconciliation is the safety net under the fan-out. Channels lag, ACKs get lost, and a thin buffer still occasionally oversells. So on a fixed cadence the engine pulls each channel’s reported stock and price back, diffs them against what it believes it published, and corrects the drift — re-pushing the right number where the channel fell behind, flagging any SKU that sold below the floor or below zero. Every correction is an event, so a recurring drift on one channel becomes a measurable pattern, not a midnight surprise.
The payoff lines up against the costs the broken-truth problem was bleeding:
- Time-to-sellable on all channels12–21 days2–5 daysScored + auto-enriched publish
- Published-but-broken SKU rate6–12%1–3%Per-channel readiness gates
- Oversell / cancellation rate2–5%0.3–1%Atomic sellable + reconcile loop
- Cross-channel price-mismatch windowhours< 5 minPrice fan-out + reconcile
Runbook: adding a new sales channel to the publish fabric
The architecture earns its keep the day Marketing signs a new marketplace and expects the catalog live next week. With the engine, onboarding a channel is a sequence of declarations, not a four-month connector project — because the readiness rules are data and the adapter is dumb.
- Declare the channel contract. Insert the rows: required attributes per category, value vocabulary and mappings, media spec, price rules (floor, MAP, currency), and stock semantics + safety buffer. This is the channel, expressed as policy.
- Dry-run the score. Run readiness for the existing catalog against the new contract without publishing. The output is a triage list: how many SKUs are Publish-ready, how many are Enrich-recoverable, and how many Hold — and exactly which attribute or image spec is blocking the recoverable set.
- Pre-enrich the recoverable tail. Let the enrichment lane work the dry-run holds: infer the missing mappings, generate the channel title variants, crop the imagery. Re-score. The Publish-ready count climbs without a merchandiser touching a row.
- Wire the adapter and sync. Connect the channel’s publish API and subscribe it to the price/inventory fan-out. The adapter consumes the cleared verdicts; it derives no rules of its own.
- Open the gate gradually. Publish the Publish-ready tier first, watch the channel’s ACK and rejection rate against your dry-run prediction, then release the enriched tier. Hold stays human until the channel’s behavior is trusted.
| Role | RACI | Responsibility |
|---|---|---|
| Omnichannel / PIM Lead | A | Owns channel contracts, readiness thresholds, enrichment promotion policy |
| Commerce Platform Architect | R | Builds the readiness engine, sync fabric, adapters, event backbone |
| Channel / Marketplace Ops | R | Runs the Hold queue, validates dry-run triage, manages channel relationships |
| Data Science | R | Owns the enrichment models and per-class confidence calibration |
| Merchandising | C | Approves regulated-attribute holds and promotes enrichments into the PIM |
| Security / Privacy | C | Reviews pricing-data handling and channel credential scope |
Local-first is the right default for the high-frequency core: readiness scoring, the sellable decrement, and the reconcile diff run on millions of SKU-channel pairs and should sit close to the record on infrastructure you control — no per-call external tax on routine validation, and pricing logic that never leaves your boundary to be checked. Reserve external models for the genuinely open-ended enrichment — the title rewrite, the ambiguous category call — where their judgment adds something the rules can’t.
What we covered
A SKU is finished when a shopper can put it in a cart on whatever surface they happen to be holding — and when the next sale, price cut, and return don’t quietly break it again somewhere else. Get the publish-readiness engine and the sync fabric right, and “live everywhere” finally means “sellable everywhere.”
References: GS1 / GTIN governance · marketplace category & media requirements · MAP / pricing policy · OMS available-to-promise standards · NIST AI RMF · NIST Privacy Framework · NIST CSF 2.0 · GDPR · CISA Secure by Design.