Description
Exports a selected set of commercetools products (by product selection, category, product type, SKU, product key, store assortment or query predicate) for commercetools AI Hub across vendors (OpenAI Commerce feed, OpenAI Ads feed, Google Merchant Center and Microsoft Merchant Center, including supplemental feeds). Downloads once, then works offline. Maps product data to each vendor spec, checks every vendor's required fields, writes a working file per vendor for the business team to complete, and converts the completed file into the vendor upload format (.jsonl.gz, .tsv, .txt) with upload guidance, including OpenAI manual upload. Use when exporting products for ChatGPT shopping or AI Hub, checking feed readiness or missing required fields, building a supplemental feed, or converting an edited feed file for upload. Not for building or deploying the Connect feed connector itself (see commercetools-connect) and not for loading a catalog into commercetools (see commercetools-catalog-migration).
Installation
In any Claude Code session, run the following commands one at a time:
-
Add the commercetools AI marketplace:
/plugin marketplace add commercetools/commercetools-ai-plugins -
Install the commercetools AI plugin (Skills + Subagents + MCP):
/plugin install commercetools@commercetools
If you've updated the plugin or installed it in another window and need the current session to pick up the latest version:
/reload-plugins
commercetools/commercetools-ai-plugins. Then, click on the plugin and click Install.Instructions Included
commercetools AI Hub product feeds
AI Hub feeds go to several vendors: OpenAI Commerce feed (ChatGPT shopping), OpenAI Ads feed, Google Merchant Center and Microsoft Merchant Center. "AI Hub" names the whole export, not one vendor.
aihub_feeds.py fetch commercetools ─► <run>/raw/ products, categories, Store, manifest
aihub_feeds.py build raw + mapping + specs ─► <run>/working/<vendor>-working.csv, report/, NEXT-STEPS.md
(business team completes the working files offline)
aihub_feeds.py finalize completed file ─► <out>/<vendor upload file> + finalize report
scripts/aihub_feeds.py (Python 3.9+, standard library only) is the whole toolchain; run
aihub_feeds.py <command> --help for options. The network is used for the download and to
read the vendors' current specs; the specs are cached per run, so rebuilding and finalizing
work offline.python3 scripts/aihub_feeds.py (py -3 on Windows, where python3 is often
missing or a Microsoft Store stub). When no Python 3.9+ is found, tell the user to install it
from python.org and stop; do not rewrite the script in another language.build and finalize read each vendor's published spec
at runtime (fields, required/conditional/optional, allowed values, length limits, Microsoft's
supplemental fields) and merge it with the code that maps commercetools data to fields. New
spec fields become empty columns, removed ones are dropped, and the report lists both. See
references/vendors.md.Data integrity: commercetools is the only source of product data
Feeds are checked against the storefront, and a guessed value hides a real gap. So:
- Do not write, rewrite, shorten, translate, summarise or infer product content: titles,
descriptions, brands, GTINs, MPNs, categories, colors, sizes, conditions, availability,
prices or images. Not in
mapping.json, not in the working files, not in the completed file. mapping.jsonchanges only where a value is read from (attribute names, category and Store fields, id strategy) and how an existing value is spelled for a vendor (valueMaps, units, separators). It holds no values of its own.- Feed eligibility flags are merchant settings, not product data, and the only defaults:
discovery on (
is_eligible_search=true), checkout off (is_eligible_checkout=false) and, for the OpenAI Ads feed only,is_ads_eligible=true. A product attribute overrides them; change a default only when the user asks. Google and Microsoft have no checkout flag; their products are listed for discovery by default. - Storefront URL, seller name and policy URLs (privacy, terms, refund) come from the Store
passed with
--store, the Store whose product selections define the assortment. - Values the business team adds in the working file are theirs. Convert the completed file
as it is:
finalizechanges the format, never the content. If a value looks wrong, report it; do not correct it.
Before you start
-
Docs search (required, run first) — The first time you use this skill in a session you must run this before answering. It gathers the latest verified documentation as your primary grounding, filtered to the products this skill covers. Use this script for documentation search while working with this skill; the Knowledge MCP covers everything else. Always confirm details against retrieved documentation rather than the skill text alone:
node "<this skill's directory>/scripts/docs-search.mjs" \ --query "<extract key terms: AI Hub, product feed, Product Projections, price selection, Product Selections>" \ --app-name "<host app: claude-code, claude-chat, cursor, codex, copilot — or the host's own name>" \ --model "<current-model>" \ --commercetools-project-key "<if known>" \ --commercetools-region "<if known>" \ --limit 10Pass the commercetools project key and region (as inapi.{region}.commercetools.com) only if already in your context; otherwise omit both. Never search files or ask the user for them. -
Vendor specs change often, which is why they are read at runtime. When the user asks whether a spec changed, or a vendor flags a field the report marked valid, rebuild the run with
--specs liveand report its Changes since the previous spec section. If a spec page could not be read, the report says which fallback was used and links the page.
Workflow
-
Agree the selection and market. Collect, asking only for what is missing:
- credentials: a dotenv file with the
CTP_*variables. Pass it with--env-fileand never print or copy secret values; - Store, currency, country, distribution channel, locale.
aihub_feeds.py fetch --list-storesprints the project's currencies, countries, languages and channels and every Store with its languages, countries, channel keys, product selections and storefront fields; - filters: any combination of product selections, categories, product types, SKUs,
product keys, attribute conditions (
--attribute color=blue), query predicates (--where) and search filters (--search-filter, e.g. inventory or price ranges).--list-categoriesand--list-attributemap the user's wording to keys. When wording could mean different data (stock as inventory or as an attribute), show both counts and ask; - vendors: the user names them; if the request does not say, ask, for example "Which
vendors should I build feeds for: OpenAI Commerce feed, OpenAI Ads feed, Google
Merchant Center, Microsoft Merchant Center?" Never assume all of them, and do not label
one vendor "AI Hub".
buildrequires--vendors(openai,openai-ads,google,microsoft);
Flags, scopes and filter recipes: references/selecting-products.md. - credentials: a dotenv file with the
-
Resolve, then download. Run
aihub_feeds.py fetch --dry-runfirst. It writes nothing, shows what each name resolved to, counts matching products, samples whether they are in the Store assortment, and checks the feed locale against the data. Show the user the plan and any warnings, then run the download once they confirm. A name that matches several categories stops with the candidates: ask which one is meant (a dry run over the union shows whether the choice changes the result). Ignoring the Store assortment changes the scope, so ask before using--ignore-assortment. -
Build the working files.
aihub_feeds.py build --run <run> --vendors <named vendors>writes one working file per requested vendor,report/gap-report.md(required fields per vendor first),report/issues.csvandNEXT-STEPS.md. The first build writes<run>/mapping.json. -
Review the gaps with the user. For each vendor, report which required fields are filled and which are missing, with example SKUs, and the
⚠?fields whose spec condition may apply (e.g. required for apparel) for the user to judge. Sort gaps into:- mapping gaps, where the value exists in commercetools under another name or
spelling: point
attributes.*(orstore.*,categoryFields) at it, or add avalueMapsentry. Confirm with the user, rebuild offline, show before/after counts. See references/mapping-and-files.md; - values commercetools does not have: they stay blank in the working file, marked in
_missing_required. The team fills them in the file or in commercetools.
- mapping gaps, where the value exists in commercetools under another name or
spelling: point
-
Hand over the working files. Point to
working/andNEXT-STEPS.md: how to edit the files safely (import as text so GTINs and SKUs keep their form, keep the header row) and what each helper column means. -
Finalize a completed file. When the user hands a completed file back, run
aihub_feeds.py finalize --vendor <v> --input <file> --out <dir>(Google and Microsoft:--kind supplemental|primary, optional--columns). When the user wants an upload file straight away, finalize the generated working file as it is. Report the finalize report as facts: rows, required fields still missing or invalid, empty cells in supplemental columns, columns a vendor does not accept. Then give the vendor's upload guidance from its reference. Do not decide for the user whether to upload.
Facts to surface (information, not decisions)
| Topic | Fact | Where |
|---|---|---|
| OpenAI Ads feed | A separate feed in the ad account (its own SFTP), not the merchant portal feed. Products need is_ads_eligible=true; ads_metadata keys come from the Ads setup and are never invented. | vendors.md |
| OpenAI uploads | Each upload is treated as the complete catalog; products missing from a later upload drop out after up to 14 days. | vendors.md |
| Supplemental ids | Supplemental rows apply only to products whose id matches the primary feed. A SKU-vs-key mismatch updates nothing and reports no error. | mapping-and-files.md |
| Supplemental cells | Every column overrides the primary value, and an empty cell can clear it. --columns limits the file to chosen attributes. | vendors.md |
| Microsoft supplemental | Accepts only the fields Microsoft lists for supplemental feeds (read at runtime; id, title, description, size, color, custom_label_0–4, promotion_ID when captured); other fields belong in the primary feed. | vendors.md |
| Market | Prices come from price selection (currency, country, channel); availability from inventory, blank without inventory entries. | selecting-products.md |
Reference index
| Topic | Reference |
|---|---|
Credentials, scopes, --list-stores, filters, store assortment, market context, dry run | references/selecting-products.md |
mapping.json, working files, finalize, reading the gap report | references/mapping-and-files.md |
| Runtime vendor specs, per-vendor facts, file formats, upload steps (OpenAI manual upload, OpenAI Ads feed, Google and Microsoft supplemental feeds) | references/vendors.md |
Scope
--staged) product data, variant-level rows,
price selection, inventory-based availability, Store and seller data, required-field checks
per vendor, working files for offline completion, conversion of completed files into vendor
upload formats, upload guidance.commercetools-connect), shipping and tax settings (configured per account in each
merchant center), and changing product data in commercetools.Checklist
- The vendors were named by the user (asked when missing, listing OpenAI Commerce feed, OpenAI Ads feed, Google and Microsoft), and only those were built.
- Flags stayed at their defaults (discovery on, checkout off, ads on only for the OpenAI Ads feed) unless the user asked otherwise.
- The selection, Store, currency, country and locale were confirmed with the user, and the dry run resolved every product selection, category and product type.
- The download used an API client with
view_productsandview_categories(plusview_product_selections/view_storeswhen used), and no secret was printed. - Per vendor, the user knows which required fields are filled and which are missing,
with example SKUs. Mapping gaps were fixed in
mapping.jsonwith the user's agreement. - No product content was written or inferred;
mapping.jsononly points at attributes and fields; the completed file was converted without changing its values. - The report says which spec was used per vendor (live with fetch time, or the built-in list when the fetch failed); new or removed spec fields were mentioned.
- The user has the working files,
NEXT-STEPS.md, the finalize report when a completed file was converted, and the upload guidance for each vendor.
References
Mapping, working files and finalize
Contents
- Building
- Item and group ids
- URLs and seller data
- Attributes
- Feed flags
- Value maps and availability
- Variants and titles
- Categories
- Working files
- Finalizing a completed file
- Reading the gap report
Building
aihub_feeds.py build merges <run>/mapping.json over the default mapping built into the
script (objects merge, lists replace) and writes the effective result back to <run>/mapping.json.
Edit that file and rebuild.python3 "<this skill's directory>/scripts/aihub_feeds.py" build --run ai-hub-feeds/<run> \
--vendors <the vendors the user named> [--mapping other.json] [--specs auto|live|bundled]
--vendors is required and has no default: build only for the vendors the user asked for
(openai, google, microsoft).mapping.json decides where values are read from and how they are spelled for each vendor.
It holds no values of its own and no defaults: when no attribute or field matches, the value
stays blank and is reported.<run>/normalized/records.jsonl (keys such as item_id, sku, title,
description, url, brand, gtin, price_amount, currency, sale_price_amount,
availability, category_path, group_id, variant_options, seller_name), then formats
it per vendor. When a vendor value looks wrong, check the normalized record first: it shows
whether the cause is the mapping or the vendor formatting.Item and group ids
| Key | Default | Use |
|---|---|---|
itemId | sku | sku, variantKey, productKeyVariantId or template |
itemIdTemplate | {productKey}-{variantId} | Used when itemId is template |
groupId | productKey | productKey, productId or slug; set only on products with more than one variant |
itemId must reproduce the id of the merchant's
existing primary feed. Ask the user for two or three ids from their merchant center and
compare them with normalized/records.jsonl before handing over the files. A wrong id
strategy updates nothing, and neither merchant center reports it as an error.The report warns on duplicate ids, which usually means variants without SKUs.
URLs and seller data
--store. Each store.<key> is an ordered list
of paths into the Store; the first
non-empty one wins. custom.<name> reads a Store custom field, and a trailing @origin
keeps only the scheme and host of a URL.| Key | Default paths | Feeds |
|---|---|---|
store.baseUrl | custom.sellerStoreUrl, storefront.orderUrlTemplate@origin, storefront.checkoutUrlTemplate@origin | {baseUrl} in product URLs; OpenAI seller_url |
store.sellerName | custom.sellerName, name | OpenAI seller_name (required) |
store.privacyPolicyUrl, store.termsOfServiceUrl | storefront.privacyPolicyUrl / storefront.termsOfServiceUrl, then custom fields | OpenAI, required when checkout is enabled |
store.returnPolicyUrl | storefront.refundPolicyUrl, custom.returnPolicyUrl | OpenAI return_policy |
store.returnWindowDays, store.acceptsReturns | custom.returnWindowDays, custom.acceptsReturns | OpenAI; the window is only sent when returns are accepted |
"store": { "baseUrl": ["custom.shopUrl"] }.productUrlTemplate ({baseUrl}/products/{slug}) sets the shape of product page URLs.
Add ?sku={sku} for variant-specific landing pages, which Google prefers. Placeholders:
{baseUrl} {slug} {sku} {variantId} {variantKey} {productKey} {productId} {locale}. When
{baseUrl} is used and the Store has no storefront URL, product URLs stay empty.Attributes
attributes.<field> is an ordered list of commercetools attribute names. Matching ignores
case, variant attributes win over product-level attributes, and the first non-empty value is
used. Values are rendered as follows: enum and lenum use the label, localized values the
feed locale, money becomes amount CUR, and references the expanded name or key.| Field | Default names | Feeds |
|---|---|---|
brand | brand, manufacturer, designer | all |
gtin, mpn | gtin, ean, upc, barcode / mpn, manufacturerPartNumber | all; GTINs are checked for length and check digit |
color, size, sizeSystem, material, pattern | same names | all |
gender, ageGroup, condition | same names | all, after valueMaps |
weight, length, width, height (+ weightUnit, dimensionsUnit) | same names | OpenAI, Google |
isEligibleSearch, isEligibleCheckout | chatgptEnableSearch, chatgptEnableCheckout | OpenAI |
reviewCount, starRating | reviewCount / starRating, averageRating, rating | OpenAI |
googleProductCategory | googleProductCategory | Google, Microsoft (wins over categoryFields) |
identifierExists | identifierExists | Google identifier_exists, Microsoft identifier_exists; never derived from a missing GTIN |
promotionId, customLabel0–customLabel4 | promotionId / none | Google, Microsoft |
"250 g" or "20 cm", and
{ "value": 1.2, "unit": "kg" }. A bare number needs a unit from a weightUnit /
dimensionsUnit attribute. Without one the measure stays blank and is noted in the report.Feed flags
flags holds the merchant's eligibility settings, used when a product has no attribute for
them: isEligibleSearch (true, discovery), isEligibleCheckout (false) and
isAdsEligible (true, written only in the OpenAI Ads feed). Attributes in
attributes.isEligibleSearch / isEligibleCheckout / isAdsEligible override per product.
The report header lists the defaults and how many rows used them. Change a default only on
the user's request.adsMetadata maps OpenAI Ads metadata keys to attribute names, e.g.
{"custom_label_0": ["season"]}; use only keys the Ads integration is configured for.Value maps and availability
valueMaps.gender|ageGroup|condition: re-spells existing project values (case-insensitive) as spec values, for example"herren": "male". Only map values that mean the same thing. Values that are still outside the spec stay in the working file as they are and are flagged in_issues.- There is no default
condition. Vendors treat an empty condition as new; products sold used or refurbished need aconditionattribute in commercetools. - Availability comes only from inventory.
restockableAsBackorder(false) sends out-of-stock variants withrestockableInDaysasbackorder;onlyInStock(false) drops out-of-stock variants.
Variants and titles
variantOptions lists the attributes that can distinguish variants, with display labels
(default color, size, material, pattern). Only options whose values differ between a
product's variants are used, and they become OpenAI variant_dict.appendVariantToTitle
(false) appends the varying option values, such as Classic Tee - Red / M. It only
reuses commercetools values, but it changes the title, so turn it on only when the user asks.
Titles over a vendor's length limit are flagged, never shortened.Categories
product_category (OpenAI) and product_type (Google, Microsoft) use one category path per
product, for example Women > Clothing > Dresses. With categoryPath.preferFilteredCategories
(default true) a category inside the downloaded --category tree is preferred; categories
under the roots listed in categoryPath.excludeRoots (root category keys or names, e.g.
["new", "sale"] for merchandising trees) come last. The report notes when every path starts
with the filtered category, the usual sign that it is a merchandising tree. Otherwise the commercetools order applies. The
Google product taxonomy value comes
from the product attribute in attributes.googleProductCategory, otherwise from the
category custom fields listed in categoryFields.googleProductCategory. The most specific
category wins, walking from the assigned category up to its root, so a value on a top-level
category covers its whole tree.{
"productUrlTemplate": "{baseUrl}/{locale}/p/{slug}?sku={sku}",
"attributes": { "brand": ["brandName"], "gtin": ["ean13"], "customLabel0": ["season"] },
"categoryFields": { "googleProductCategory": ["googleCategoryId"] },
"store": { "baseUrl": ["custom.shopUrl", "storefront.orderUrlTemplate@origin"] },
"categoryPath": { "excludeRoots": ["new", "sale"] }
}
Working files
working/<vendor>-working.csv (UTF-8 with BOM, comma-separated) has one row per variant and
one column per vendor field, in spec order with the id first, including fields commercetools
has no values for, so the team can fill them. Two helper columns lead:| Column | Content |
|---|---|
_missing_required | Required fields this row has no valid value for |
_issues | Other findings: invalid optional values, over-long text, recommended fields missing |
79.99 USD, in_stock, in stock for Microsoft), flagged
values are kept as they are, and lists use commas (%2C inside URLs). OpenAI object fields
(variant_dict, dimensions) are JSON text.NEXT-STEPS.md: import as text so GTINs, SKUs and prices
keep their exact form (spreadsheets otherwise turn 00012345600012 into 1.23456E+10),
keep the header row, and save as CSV UTF-8. Comma-, semicolon- and tab-separated files are
all read back.Finalizing a completed file
python3 "<this skill's directory>/scripts/aihub_feeds.py" finalize --vendor openai \
--input <completed>.csv --out <dir> [--format jsonl.gz|csv.gz|tsv.gz]
python3 "<this skill's directory>/scripts/aihub_feeds.py" finalize --vendor google|microsoft \
--input <completed>.csv --out <dir> [--kind supplemental|primary] [--columns brand,gtin,...]
finalize reads the run's cached specs (<run>/specs, next to working/; --specs-dir
otherwise), drops the _ helper columns and columns that are not vendor fields,
writes every row with its values exactly as given, and converts only the format:| Vendor | Output |
|---|---|
| OpenAI Commerce feed | product-feed.jsonl.gz (typed JSON: booleans, numbers, arrays, objects), or .csv.gz / .tsv.gz |
| OpenAI Ads feed | ads-product-feed.csv (UTF-8 CSV), or `--format jsonl.gz |
google-supplemental-feed.tsv (id first), or google-primary-feed.tsv with --kind primary | |
| Microsoft | microsoft_supplemental_feed.txt (only the fields Microsoft accepts there, id last) or microsoft_primary_feed.txt with --kind primary |
--columns restricts them. It then re-runs the vendor checks and writes
<vendor>-<kind>-finalize-report.md: rows with every required field, required fields still
missing or invalid, empty cells per supplemental column, duplicate or missing ids, and
columns that were not written (not accepted by the vendor, not requested, or empty in every
row). Mention columns that were empty in every row, e.g. no descriptions to send. Report these as facts; the decision to upload is the user's.Reading the gap report
The report starts with a summary per vendor: rows, rows with every required field, rows missing at least one. Each vendor section then has:
- Required fields: every always-required and conditionally required field with filled /
missing / invalid counts;
✓when complete,⚠when a required value is missing or invalid,⚠?when the field is empty and the spec's condition (shown in When) may apply; - All fields: Filled, Flagged (sent with a note or re-spelled for the vendor, such
as an over-long title or
backorderasout of stockfor Microsoft), Invalid (kept, but outside the spec) and Missing, plus the commercetools source of each field; - Top issues with example ids, and where each missing value would come from.
id. report/issues.csv has one line per row and field.Selecting products
aihub_feeds.py fetch is the only command that calls commercetools. It is read-only and writes
everything later steps need into <run>/raw/, so the build can be repeated offline.Credentials and scopes
--env-file:| Variable | Required | Notes |
|---|---|---|
CTP_PROJECT_KEY, CTP_CLIENT_ID, CTP_CLIENT_SECRET | yes | API client credentials. |
CTP_AUTH_URL, CTP_API_URL | yes | https://auth.{region}.commercetools.com, https://api.{region}.commercetools.com. |
CTP_SCOPE | no | Space-separated; :{projectKey} is appended when missing. Omit to get all scopes of the client. |
CTP_STORE_KEY, CTP_CURRENCY, CTP_COUNTRY, CTP_LOCALE, CTP_DISTRIBUTION_CHANNEL_KEY | no | Defaults for --store, --currency, --country, the feed locale and --channel. |
view_products: product projections, product types, channels;view_categories: category paths and--categoryfilters. Without it the download still runs, but category filters fail andproduct_category/product_typestay empty;view_product_selections: with--product-selection, and with--storewhen the Store has product selections;view_stores: only with--store.
Pass the dotenv file by path. Do not read it into the conversation or echo its values.
Finding the market context
python3 "<this skill's directory>/scripts/aihub_feeds.py" fetch --env-file <path to .env> --list-stores
storefront / custom fields it has, and every product selection with
its mode and product count. Two more listings map the user's wording to keys:--list-categories [TEXT]:key: pathfor every category whose path contains TEXT, e.g.--list-categories "women > shoes";--list-attribute NAME: the attribute's type and, for enums,key: labelpairs, e.g.--list-attribute color. Use it to propose--store,--currency,--country,--channeland--localeinstead of guessing, and confirm them with the user.
Market context
These decide which price and stock a row carries, so confirm them before downloading.
--currency,--country,--channel(distribution channel key),--customer-groupturn on price selection. Each variant then carries the one best-matchingprice, including Standalone Prices, and its discounted value. Without--currencythe build picks a price offline from embedded prices and flags it in the report.--storeaddsstoreProjection, which limits locales, prices and inventory to the Store's configuration (Product Projections), and applies the Store's active Product Selections:Individualselections include only assigned products,IndividualExclusionselections remove excluded ones, and variant selections (includeOnly,includeAllExcept) limit SKUs. Use--ignore-assortmentto export products outside the Store's assortment. That changes the scope of the export, so it is the user's call: when the dry run reports that the selection and the assortment do not overlap, ask. The same Store supplies the storefront URL used in product links, the seller name and the privacy, terms and refund policy URLs. Without--storethese fields stay empty. A Microsoft supplemental feed carries none of them, so a Store matters there only for prices, inventory scope and assortment.- The feed locale (
--locale, elseCTP_LOCALE, else the Store's first language, elseen-US) is used for names, descriptions, slugs and localized attribute values. A locale falls back to its language (en-USreadsen) and to other locales of that language. When a product has no text in that language the value stays blank and the row is listed under Normalization notes.allowLocaleFallback: truein mapping.json uses another language instead; only set it when the user accepts text in another language in the feed. - Availability comes from inventory on the variant. With several supply channels a variant is in stock when any channel is. Variants without inventory entries get no availability and are reported.
Filters
All filters are combined with AND. Category, product type, SKU and product-key values may be repeated or comma-separated.
| Need | Flags |
|---|---|
| One category tree | --category mens (id, key, slug or name; sub-categories included, --no-subtree to disable). A name or slug used by several categories, such as "Women" under both Women and Sale, stops with the candidate keys and paths: show them to the user and re-run with the chosen key |
| Several categories | --category shirts --category "Kitchen Knives" |
| Product type | --product-type apparel (key, id or name) |
| Specific variants | --sku TEE-RED-M,TEE-RED-L (only those variants become rows) |
| Specific products | --product-key classic-tee |
| One product selection | --product-selection "Summer picks" (key, id or name; only the products, and variants, it includes) |
| Store assortment | --store us-store (all of the Store's active product selections) |
| Attribute value (any type) | --attribute color=blue, --attribute designer=michaelkors,karllagerfeld, --attribute size>=40, --attribute isOnStock=true |
| In stock (inventory in any supply channel) | --search-filter "variants.availability.isOnStockInChannels:exists" |
| In stock in one channel | --search-filter 'variants.availability.isOnStockInChannels:"<channel id>"' |
| In stock (inventory without supply channel) | --search-filter "variants.availability.isOnStock:true" |
| Price below 200 (selected price) | --currency USD --country US --search-filter "variants.scopedPrice.currentValue.centAmount:range(* to 19999)" (range ends are inclusive; currentValue is after Product Discounts, value before, so ask which should count) |
| Any embedded price below 200 | --where 'masterVariant(prices(value(currencyCode="USD" and centAmount < 20000)))' (any matching Embedded Price, not necessarily the one the feed carries) |
| Recently changed | --where 'lastModifiedAt > "2026-10-01T00:00:00.000Z"' |
| Sample | --max-products 100 |
| Draft data | --staged |
--product-selection and --store can be combined: a product must be in both. Use
--store as well when the export should carry the Store's storefront URL, seller and
policy data, or its price and inventory scope.Three ways to filter
| Mechanism | Endpoint | Use for | Variant handling |
|---|---|---|---|
--attribute | Product Projections query | Any product or variant attribute, by name, typed from the product types | Only matching variants (--attribute-scope product keeps whole products) |
--where | Product Projections query | Anything query predicates can express: product fields, embedded prices, dates, keys, nested attributes | Whole products |
--search-filter | Product Projection Search | What predicates cannot: inventory (variants.availability.isOnStock, availableQuantity), selected prices (variants.scopedPrice…), attribute facets and ranges | Only matching variants (--all-variants keeps all) |
--where and --search-filter use different endpoints and cannot be combined; categories,
product types, SKUs, product keys, product selections and --attribute work with both
(attribute conditions are sent as predicates or search filters, then checked per variant).
All filters are ANDed.--where or
--search-filter. Use the scoped-price filter when the cut-off should match the price the feed
carries, and --where when any price in a currency is enough.--search-filter
fails: use --where or --attribute, and for stock read the availability column of the
working file instead of filtering.availability is built from) or a project attribute such as isOnStock. When both exist,
they usually give very different results: show the user both counts and ask.--attribute conditions must hold on the same variant (one combined predicate).
Conditions the query cannot express (!=, localized text, money) are checked per variant only;
the dry run then warns that its match count still includes products they will drop.--attribute name<op>value: operators =, !=, >, >=, <, <=; = and != take a
comma-separated list (color=blue,black). The attribute type comes from the product types:
enum and lenum values match by key or by label in any locale (gender=Herren means key
men), numbers and dates compare as such, set attributes match when any element matches.
An unknown attribute name or enum value stops with the valid names or keys, so a dry run
is the way to map the user's wording ("blue shirts by a given designer") to attribute names and
keys; it prints the resolved conditions. Confirm the mapping with the user when the wording
could mean more than one attribute (for example stock as an isOnStock attribute versus
inventory).variants.attributes.<name>:"value" (text), variants.attributes.<name>.key:"value"
(enum), range(<from> to <to>) with * for open ends, money in cent amounts, and scoped-price
filters only after --currency (and --country) select a price.sort=id asc, id > "<last>"), so there is no offset limit on
large catalogs.Running it
python3 "<this skill's directory>/scripts/aihub_feeds.py" fetch \
--out ai-hub-feeds/<run> \
--env-file <path to .env> \
--store <store-key> --currency USD --country US --channel <channel-key> \
--category <category> --dry-run
--dry-run writes nothing and needs no --out. It prints a short summary (resolved names,
search filters, number of predicates, products matching the query before the Store
assortment and per-variant checks, variants kept in a sample, warnings); add --json for the
full plan with the query. It resolves product selections, categories, product types and
attribute conditions and prints, in addition to the query (endpoint, predicates or search
filters), sampleVariantsKept when variant filters apply, and: what each name resolved to (resolvedCategories as
key: path, resolvedProductSelections), how many products match the query
(productsMatchingQuery), how many sampled products are in the Store assortment / product
selection (sampleInAssortment), and whether the feed locale matches the sampled product
names (localeCheck). It stops with a message for unknown product selections, categories
or product types: ask the user which one they meant rather than guessing. Remove --dry-run to download.<run>/raw/:| File | Content |
|---|---|
products.jsonl | One Product Projection per line, with _feed.selectedSkus when only some variants are in scope |
categories.json | All categories, for category paths and category mapping |
store.json | The Store: name, storefront URLs and policy URLs, custom fields, product selections |
fetch-manifest.json | Resolved filters, query, counts, warnings. No credentials |
createdBy, lastModifiedBy, which carry the API client id) is removed
before anything is written. The run folder still contains catalog data. Keep it out of version control.Vendors: specs, files and uploading
Contents
- Specs are read at runtime
- OpenAI Commerce feed
- OpenAI Ads feed
- Google Merchant Center
- Microsoft Merchant Center
Uploading is the merchant's step; the upload sections below are guidance for them. If the user asks the agent to upload or publish (SFTP, a hosted URL, a Merchant Center data source), first show the target, the file and its effect (an OpenAI Commerce upload replaces the whole catalog, a supplemental feed overrides primary values, an Ads feed can put products into live campaigns), and act only after the user's explicit yes.
Specs are read at runtime
aihub_feeds.py build (and finalize) fetch the
vendor's published spec, parse it and cache it in <run>/specs/<vendor>.json; later offline
builds of the same run use the cache. aihub_feeds.py specs --vendors <v> shows the current
spec on its own.| Vendor | Spec read from |
|---|---|
| OpenAI Commerce feed, OpenAI Ads feed | developers.openai.com/commerce/specs/file-upload/products (Markdown version, .md); Ads rules from the OpenAI Ads product feeds guide |
| support.google.com/merchants/answer/7052112 (attribute tables) | |
| Microsoft | Microsoft product attributes and Microsoft feed files and supplemental feeds (fields a supplemental feed accepts) |
What stays in code is how commercetools data becomes each field (the field registry). The gap report compares both and lists:
- fields in the spec with no commercetools mapping: written as empty working-file columns the team can fill; tell the user, and add a mapping when the data exists in commercetools;
- fields no longer in the spec: left out;
- requirement changes, e.g. a field that became required.
--specs live refetches; the previous copy is kept as <vendor>.previous.json and the report
opens the vendor section with Changes since the previous spec of this run (fields added,
removed, requirement / allowed values / length changed). Use this when the user asks whether
a spec changed. --specs bundled uses the registry's own list (captured 2026-10-23).When a spec cannot be fetched or no longer parses (page layout changed), the run's cached spec is used, else the registry's list; the console and the report say which. Tell the user and link the spec page.
⚠? in the required-fields table with the rows where the field
is empty: the merchant decides whether the condition applies.OpenAI Commerce feed
working/openai-working.csv; finalize --vendor openai writes
product-feed.jsonl.gz (or --format csv.gz|tsv.gz), every row, values as given. OpenAI
accepts gzipped CSV, TSV and JSONL (Parquet with zstd is preferred for large automated
feeds), UTF-8, up to about 500k items and ~500 MB per file.item_id; product name → title; description (HTML
stripped) → description; productUrlTemplate + Store storefront URL → url; attributes →
brand, gtin, mpn, color, size, gender, age_group, condition, measures; first /
other images → image_url / additional_image_urls; price selection → price,
sale_price; inventory → availability; product key → group_id with
listing_has_variations and variant_dict (varying options) on multi-variant products;
Store → seller_name, seller_url, policy URLs; chatgptEnableSearch/Checkout attributes →
is_eligible_search / is_eligible_checkout.is_eligible_search=true (discovery) and
is_eligible_checkout=false from the mapping's flags defaults unless a product attribute
says otherwise. The privacy policy and terms URLs only matter once checkout is true, which
also needs the checkout integration enabled with OpenAI. is_ads_eligible is not part of the
merchant feed; use the OpenAI Ads feed export for ads. If
offer_id is added, it must not contain the price.Full snapshot semantics
is_eligible_search=false. OpenAI suggests
validating about 100 items first and refreshing at least daily.Uploading (merchant portal → product feed)
Manual upload, on the product feed page of the OpenAI merchant portal:
- Manual upload → Upload a file ("Upload a compressed feed file from your device") →
Choose File → the finalized
product-feed.jsonl.gz. - Validate. Problems go back into the working file (or the mapping / commercetools), then finalize and validate again.
- Submit feed.
commercetools-connect).OpenAI Ads feed
A product feed in the OpenAI ad account, used by product-feed campaigns. It is separate from the merchant portal feed: created in the ad account (needs product feed API access), with its own SFTP access (password or SSH key), and processed for ads rather than ChatGPT discovery.
working/openai-ads-working.csv; finalize --vendor openai-ads writes
ads-product-feed.csv (UTF-8 CSV, as the Ads guide asks; --format for others). It is the
OpenAI product schema plus:| Field | From | Note |
|---|---|---|
is_ads_eligible | attribute chatgptEnableAds / isAdsEligible, else flags.isAdsEligible (true) | Required here: products Ads should process |
ads_metadata | adsMetadata in mapping.json, key → attribute names | Keys must be the ones configured for the Ads integration (e.g. custom_label_0); never invent keys |
target_countries, store_country | the download's --country | The market of the export |
true / false), as in the Ads guide's
example. Uploading: put the CSV in the ad feed's SFTP root, check upload history and
diagnostics, then select products for ad groups with product sets (brand, category, price,
ads_metadata.<key>). Prices and availability must stay current; product pages and images
must be public HTTPS URLs.Google Merchant Center
working/google-working.csv; finalize --vendor google writes
google-supplemental-feed.tsv (default) or google-primary-feed.tsv (--kind primary),
tab-separated, header row, id first.id; it cannot add or remove products. So:idmust equal the primary feed id exactly. Compare a few ids from Merchant Center with the working file before finalizing (item ids).- Rows missing required fields of the full spec still belong in a supplemental file.
Choosing columns
--columns, e.g.
--columns gtin,brand,google_product_category,custom_label_0; otherwise every column with a
value is written. The finalize report lists empty cells per column.google_product_category comes from an attribute or category custom field, never chosen
by the agent.Uploading a supplemental data source
- Merchant Center → Settings → Data sources → Supplemental sources → Add supplemental product data (some accounts need the Advanced data source management add-on).
- Upload a file (or Google Sheets / scheduled fetch for recurring updates).
- Select the primary data source(s) it applies to, save, then review Products → Needs attention.
Microsoft Merchant Center
working/microsoft-working.csv with every primary-feed field;
finalize --vendor microsoft writes microsoft_supplemental_feed.txt (default) or
microsoft_primary_feed.txt (--kind primary). File rules applied: tab-delimited .txt, one
header row, no spaces and at most 50 characters in the file name, no tabs or line breaks in
values, no HTML, id last (Microsoft rejects lines ending in a tab).Supplemental feed limits
id, and only accepts the fields Microsoft lists for
supplemental feeds (read at runtime; at capture time id, title, description, size,
color, custom_label_0–4, promotion_ID). Finalize writes only those and lists the rest as
not written. Gaps in other fields (GTIN, brand, price, availability, images) need the primary
feed: the merchant's own, or one finalized with --kind primary. Say this when the user
expects a supplemental feed to fix identifiers or prices. A Store matters for a Microsoft
supplemental export only through prices, inventory scope and assortment.Uploading a supplemental feed
- Microsoft Advertising → Tools → Merchant Center → Feeds → store → Create feed.
- Feed type: Supplemental feed (or Primary feed for
--kind primary); same Feed label / Country of sale and Language as the primary feed. - Input method: Manually upload (< 4 MB) or FTP/SFTP (< 1 GB; file name must match the feed settings) → Create feed. Feeds expire after 30 days.