Solution playbook

Sellable Everywhere: A Publish-Readiness Engine for Omnichannel SKUs

Download PDF ↓

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.

Authoritative truthPIM · ERP · OMS · DAMproduct attributes, price, inventory, imagery — owned here, never overwritten
Readiness enginescore · enrich · route · syncper-channel verdict + publish decision — the governed middle
Channel surfacesweb · marketplace · app · storeadapters render what the engine has cleared — nothing self-published
The engine reads from the authoritative record and writes only to channels — never back into the PIM. Each adapter is dumb on purpose: it publishes what the engine cleared for it and reports back what the channel accepted.

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.

  1. 01Scoredecompose readiness per (sku, channel)
  2. 02Publishall gates clear → push to channel now
  3. 03Enrichrecoverable defect → auto-fix, re-score, publish
  4. 04Holdunrecoverable / risky → queue a human with the reason
The dashed step is the human gate. Most SKUs never reach it: ready ones publish, recoverable ones are repaired and re-scored automatically, and only genuine exceptions wait for a merchandiser — with the failing dimension named.

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.

Cross-channel signalWebMarketplaceApp (same-day) Price change → fan-outpushpushpush Stock decrement on saleatomicatomicatomic Safety buffer appliedlowhighstore-scoped Reconcile vs. channel-reported stockhourlyhourly15 min
The buffer column is the lever: a marketplace that penalizes oversell harshly runs a deeper safety buffer than the owned web store. The reconcile row is what catches drift the fan-out missed.

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
Sample planning ranges to size against your own baseline, channel mix, and category rules. Hold the broken-SKU rate (quality), oversell (risk), and price-mismatch window (margin) together — improving one by loosening another is the trap.

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
RoleRACIResponsibility
Omnichannel / PIM LeadAOwns channel contracts, readiness thresholds, enrichment promotion policy
Commerce Platform ArchitectRBuilds the readiness engine, sync fabric, adapters, event backbone
Channel / Marketplace OpsRRuns the Hold queue, validates dry-run triage, manages channel relationships
Data ScienceROwns the enrichment models and per-class confidence calibration
MerchandisingCApproves regulated-attribute holds and promotes enrichments into the PIM
Security / PrivacyCReviews 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.

Let’s talk shop

Let’s build the system your operations actually need.

Custom software, intelligent workflows, and governed AI — designed around how your team really runs, not a template.

Direct founder access · fixed-scope pilots · measurable outcomes