Product Discovery
Uses the Channel3 CLI to query a catalog of 100M+ products across thousands of retailers. The CLI is generated from the API spec and stays in sync automatically.
Decision
| User wants... | Run |
|---|---|
| Find products by description, image, or both | channel3 products search --query-param "..." --limit N |
| More like this product I already found | channel3 products find-similar --product-id <ID> --limit N |
| Find a category slug to use as a filter | channel3 categories search --query-param "..." --limit 5 |
See a category's allowed attribute keys/values for filters.attributes |
channel3 categories retrieve --slug <slug> |
| Filter by a color (blue, navy, red, ...) | --filters '{"colors":{"palette":[{"hex":"#..."}]}}' (sRGB hex; never put color into filters.attributes) |
| Filter by non-color attributes (material, frame-color, ...) | channel3 categories retrieve --slug <slug> first, then --filters '{"attributes":{"handle":["Value"]}}' |
| See variant options + live stock for a product | channel3 products retrieve --product-id <ID> (read variants.options / variants.selected) — see Pattern 6 |
| Find a brand ID to use as a filter | channel3 brands search --query-param "<name>" --limit 5 |
| Wire Channel3 into their agent host directly (no CLI) | Channel3 MCP — see below |
Setup
The skill assumes the channel3 CLI is installed and on PATH.
npm install -g @channel3/cli
export CHANNEL3_API_KEY="..." # free key at https://trychannel3.com
Optional locale defaults: CHANNEL3_LANGUAGE, CHANNEL3_COUNTRY, CHANNEL3_CURRENCY env vars; per-call --config always wins.
If channel3 --version fails, fail loudly with the install instruction above — do not silently fall back to anything else.
Calling the CLI
Always pass --format jsonl --query '...'. The templates below project only decision-critical fields; default JSON is verbose. Never auto-pick the first element of a result array — project the whole array and read every entry. The projection language is JMESPath.
--query is the projection flag. The search term is --query-param. They are different things, and mixing them up is the most common mistake against this CLI.
--query applies to the whole response, so every projection starts at the wrapper key — products[], brands[], categories[], attributes[]. With --format jsonl the resulting array is flattened to one record per line.
Use --limit N to size the result set (5 for a first look, 10–20 to compare more). The CLI does not auto-paginate: it returns a single page unless you pass --page-all, which streams every page as NDJSON up to --page-limit (default 10). Don't pass --page-all for interactive product discovery — it bloats context.
Filters vs query. Anything that could be a filter belongs in --filters, not --query-param. Direct filter types: brand, color, category, price, gender, availability, age, condition, sale, dimensions, retailer. For category-specific traits — anything you'd expect as a checkbox or dropdown on a retailer's filter sidebar — discover the handle and value via Pattern 3 (categories search → categories retrieve → filters.attributes) before falling back to query terms. Reserve --query-param for what can't be enumerated: aesthetic descriptors, model names, or use cases. Rich filters with a minimal --query-param beat the opposite.
Default templates (copy-paste verbatim)
products search / products find-similar:
--limit N \
--format jsonl \
--query 'products[].{id: id, title: title, brands: brands[].name, offers: offers[].{domain: domain, price: price.price, currency: price.currency, availability: availability, url: url}, attrs: structured_attributes}'
brandsis the array of brand names (some products have multiple).offersis the full array of merchant offers. Compare acrossdomain/price/availabilityto recommend the right buy link. This multi-merchant comparison is the point of Channel3 — never collapse it tooffers[0].attrs(structured_attributes) shows what the catalog extracted — e.g.{"color":["Navy"],"material":["Leather"]}— so the agent can judge fit without another call.- Add
commission: max_commission_rateinsideoffers[].{...}if affiliate revenue matters for ranking.
brands search:
--limit 5 --format jsonl \
--query 'brands[].{id: id, name: name, description: description, commission: best_commission_rate}'
description and commission are decision-critical: multiple brands often share a name (e.g. searching "Nike" returns two distinct IDs — Nike the manufacturer and Nike the retail store house brand, with different commission rates).
categories search:
--limit 5 --format jsonl \
--query 'categories[].{slug: slug, title: title, path: path}'
path is the ancestor chain (Furniture > Sofas); use it to disambiguate near-duplicate slugs.
categories retrieve:
--format jsonl \
--query 'attributes[].{slug: slug, name: name, values: values}'
Drops the prose description and children — the attribute table is what feeds filters.attributes.
Chaining patterns
Each pattern is call → read → decide → next call. The agent reads every entry in the result array and picks based on the criteria below. Multi-intent requests ("either brown+cyan or red/blue/white shoes") fan out to one search per intent in parallel — don't try to express them in a single call.
1. Strict brand filter
User: "Nike running shoes, no other brands."
channel3 brands search --query-param "Nike" --limit 5 --format jsonl \
--query 'brands[].{id: id, name: name, description: description, commission: best_commission_rate}'
Read all results. Pick the id that matches user intent:
- Match
nameexactly first. - When multiple results share the name, read each
description. "Nike the manufacturer" vs "Nike retail store house brand" are different IDs; pick the one matching intent. - If
commissionmatters (affiliate revenue), factor it in — but only after correctness. - Still ambiguous? Pass multiple IDs (
brand_idsis an array, OR semantics) or ask the user. - No match? Try a query variant (typos, common variants) before telling the user the brand isn't catalogued.
channel3 products search --query-param "running shoes" --limit 10 \
--filters '{"brand_ids":["<id chosen above>"]}' \
--format jsonl \
--query 'products[].{id: id, title: title, brands: brands[].name, offers: offers[].{domain: domain, price: price.price, currency: price.currency, availability: availability, url: url}, attrs: structured_attributes}'
2. More like this
The agent already has products from a prior products search. Pick the id of the one the user actually referenced — not blindly the first hit.
channel3 products find-similar --product-id <chosen-id> --limit 10 \
--filters '{"gender":"female"}' \
--format jsonl \
--query 'products[].{id: id, title: title, brands: brands[].name, offers: offers[].{domain: domain, price: price.price, currency: price.currency, availability: availability, url: url}, attrs: structured_attributes}'
find-similar carries the source product's visual embedding but does not inherit your filters. If the reason you liked the source was filterable (color, brand, price), re-pass those filters explicitly.
If find-similar returns 404, the product isn't catalogued yet — fall back to products search using the product's title.
3. Discover attribute handles, then filter
Each category exposes its own attribute schema — a list of handles (material, neckline, etc.) with allowed values. categories retrieve reveals them; filters.attributes consumes them.
User: "leather sectional sofa."
channel3 categories search --query-param "sofa" --limit 5 --format jsonl \
--query 'categories[].{slug: slug, title: title, path: path}'
Read all results. Pick the slug whose path matches user intent (Furniture > Sofas, not Outdoor > Sofas unless they said outdoor). Multiple slugs are fine when scope is broader (e.g. sofas,sectionals).
channel3 categories retrieve --slug sofas --format jsonl \
--query 'attributes[].{slug: slug, name: name, values: values}'
Read all attribute rows. Pick non-color handles and verbatim values. (Color always goes through filters.colors — see Pattern 4.)
channel3 products search --query-param "sectional" --limit 10 \
--filters '{"category_ids":["sofas"],"attributes":{"material":["Leather"]}}' \
--format jsonl \
--query 'products[].{id: id, title: title, brands: brands[].name, offers: offers[].{domain: domain, price: price.price, currency: price.currency, availability: availability, url: url}, attrs: structured_attributes}'
4. Filter by color
User: "blue Nike sneakers."
Map the color name to its closest sRGB hex (blue → #0066CC, navy → #001F3F, etc.) and pass it through filters.colors.palette. No category lookup needed for color. Multiple entries match products containing those colors. Only add "percentage": N when the user expresses an asymmetric balance between colors (e.g. "mostly red with some green", "70% red 30% blue"); plain "mostly red" or "mostly red and green" doesn't need it. For "multicolor" or "any color", omit the colors filter entirely.
channel3 products search --query-param "sneakers" --limit 10 \
--filters '{"colors":{"palette":[{"hex":"#0066CC"}]}}' \
--format jsonl \
--query 'products[].{id: id, title: title, brands: brands[].name, offers: offers[].{domain: domain, price: price.price, currency: price.currency, availability: availability, url: url}, attrs: structured_attributes}'
5. Refinement when results don't match intent
If a search returns zero or clearly off-intent results, run another search with relaxed or shifted filters/query. Don't ask the user mid-loop — keep iterating. Common moves:
- Zero results → drop
percentageif you added one, then drop the most restrictive filter (usuallycolorsorattributes). - Wrong category drift ("sectional sofa" returned accent chairs) → add
category_idsvia Pattern 3. - Brand drift (asked for one brand, got many) → add strict
brand_idsvia Pattern 1. - Mostly out of stock → add
{"availability":["InStock"]}. - Wrong gender / age → add
{"gender":"..."}or{"age":["adult"]}. - Need more results of the same shape → re-run with a larger
--limit.
Stop once results are presentable — don't keep searching hoping for "better." Never validate hits by retrieving images, downloading CDN assets, or running find-similar to double-check — the filters and attrs/title are the source of truth, and the host UI will render the image for the user.
6. Inspect a product's variants and live stock
User: "does this come in XL?" / "what colors does it come in?" / "is the navy one in stock?"
products search returns the variant matrix but not stock — available is always null on search results. To see live per-value availability, retrieve the product:
channel3 products retrieve --product-id <ID> --format jsonl \
--query '{title: title, variants: {selected: variants.selected, options: variants.options[].{name: name, values: values[].{label: label, exists: exists, available: available, product_id: product_id}}}}'
Read the rows:
exists: false→ that value isn't offered with the currently selected options (e.g. the shirt exists in XL, but not in this color + XL). Not the same as out of stock.available→ live stock (InStockorOutOfStock), hydrated only on retrieve.product_idset → that value is a separate product (color-as-product-swap). To inspect it,retrievethat ID instead.variants.selected→ the configuration this response represents.
Retrieve is free, so refetch before telling the user about price/stock for a specific configuration. The CLI's retrieve doesn't take option_* selection params — resolving an arbitrary size+color combination server-side is an SDK/REST capability (see the channel3-api skill). For navigation, follow a value's product_id.
Filter cookbook
--filters takes a single JSON object. Combine fields freely. The full filter shape is documented at docs.trychannel3.com/api-reference/v1/search.
Max price --filters '{"price":{"max_price":100}}'
Price range --filters '{"price":{"min_price":50,"max_price":150}}'
Gender --filters '{"gender":"female"}'
Age --filters '{"age":["adult"]}'
Condition --filters '{"conditions":["new"]}'
Used/open-box --filters '{"conditions":["used"]}'
Any condition --filters '{"conditions":["new","used"]}'
Availability --filters '{"availability":["InStock"]}'
Include out of stock --filters '{"availability":["InStock","OutOfStock"]}'
On sale --filters '{"sale":"on_sale"}'
Max width (120 cm) --filters '{"dimensions":{"width":{"max":120,"unit":"cm"}}}'
Color (blue) --filters '{"colors":{"palette":[{"hex":"#0066CC"}]}}'
Two required colors --filters '{"colors":{"palette":[{"hex":"#0066CC"},{"hex":"#FFFFFF"}]}}'
Material --filters '{"attributes":{"material":["Leather"]}}'
Multiple values --filters '{"attributes":{"material":["Leather","Velvet"]}}'
Category --filters '{"category_ids":["shoes"]}'
Multiple categories --filters '{"category_ids":["sofas","sectionals"]}'
Brand --filters '{"brand_ids":["MpZS"]}'
Retailer by domain --filters '{"website_ids":["nike.com"]}'
Exclude brand --filters '{"exclude_brand_ids":["MpZS"]}'
Exclude category --filters '{"exclude_category_ids":["athletic-shoes"]}'
Combine --filters '{"price":{"max_price":150},"gender":"male","colors":{"palette":[{"hex":"#001F3F"}]},"availability":["InStock"]}'
gender is male or female only. conditions is a list of new / used (refurbished was removed; default is ["new"], which also matches unknown-condition offers — pass both values to disable). availability is a list of InStock / OutOfStock only (default ["InStock"]; pass both to disable). sale is on_sale. dimensions keys are length / width / height / weight, each {min?, max?, unit} with length units mm/cm/m/in/ft and weight units mg/g/kg/oz/lb. age values are newborn, infant, toddler, kids, adult.
Other flags
Locale (country / currency / language) overrides the env-var defaults. mode picks the search strategy. Default (lexical + semantic) is right for almost all calls — this skill's value is building the filters yourself. "agentic" hands planning to an LLM that decomposes a rich brief into sub-searches (multiple seconds; powers Channel3's MCP) — use it when you'd otherwise pass the user's full raw context through --query-param. "keyword" is lexical-only (disables semantic search; incompatible with image input) — niche real-time cases. --image-url adds an image to products search (combinable with --query-param for "this jacket but in blue").
--config '{"country":"GB","currency":"GBP"}'
--config '{"language":"de"}'
--config '{"mode":"keyword"}'
--image-url "https://example.com/jacket.jpg"
Anti-patterns
- Filters beat
--query-param-stuffing, but--query-param-stuffing beats dropping the constraint. Direct filter axes (brand, color, category, price, gender, availability) and category-specific attribute values (anything that would appear as a facet on a retailer's filter sidebar) belong in--filters— the optimal path to precise results. If a constraint doesn't fit a filter, include it in--query-paramrather than dropping it. (agenticmode inverts the default preference — see Other flags.) - Don't validate hits by retrieving images, downloading CDN assets, or running multimodal inspection. If
filters.colorswas applied, trust it.titleandattrsare enough to judge fit; the host UI renders the image for the user. - Don't run
products find-similarreflexively afterproducts search. If the first search produced good matches, present them. Usefind-similaronly when the user is anchored on one specific product they already saw — and re-pass the filters you care about, sincefind-similardoesn't inherit them. - Use
categories searchwhen the request has structured requirements, not for trivial queries. Single-word noun queries get routed correctly by semantic search inproducts search. Runcategories search→categories retrieve→filters.attributes(Pattern 3) when the request includes traits that would appear as facets on a retailer's filter sidebar, when strict inclusion/exclusion is required, or when the query is too generic to imply a category on its own. - Don't run
brands searchby default. Semantic search already biases toward in-brand matches when the brand name is in the query. Reach forbrands searchonly when (a) strict inclusion is required ("only Nike, not Nike-mentioning"), (b) exclusion is required ("running shoes excluding Nike"), or (c) the user is anchored on one brand ("what does Patagonia sell"). - Never use
filters.attributesfor color. Color always goes throughfilters.colorswith a hex value. Even ifcategories retrievelists acolorattribute, skip it for filtering. - Don't guess attribute handles or values. Unknown handles/values match nothing useful. Run
categories retrieve --slug <slug>first and copy non-colorhandles verbatim. - Don't use
[0]/[1]indexing to "pick a result." Project the whole array so the agent reads every entry and decides. Top-level singletons (no array) are the only exception. - Don't confuse
--querywith--query-param.--queryis a JMESPath projection over the response;--query-paramis the search term. Passing a search phrase to--queryfails with a JMESPath parse error, and a single word silently returnsnull. - Don't pass
--page-allfor product discovery. It streams every page and bloats context. Size results with--limitinstead.
Presenting results
Return the projected products to the host — most agentic-search UIs render the catalog from IDs. Synthesize into prose, a table, or a numbered list only when the user explicitly asked for a recommendation or comparison. Never paste raw JSON to the user.
Alternative: Channel3 MCP
For no-code agent integration, use the Channel3 MCP instead of the CLI when the host already supports it (Cursor, Claude Desktop, etc.). Don't recommend both — pick one.
About this skill
This skill queries the Channel3 product catalog via the official CLI. Search queries and any image URLs are sent to the Channel3 API. Buy links point to buy.trychannel3.com, which redirects to merchant sites with affiliate tracking. Avoid sending sensitive or private information in search queries.
- API docs: docs.trychannel3.com
- CLI docs: docs.trychannel3.com/cli
- Source: github.com/channel3-ai/skills
- Provider: Channel3 (trychannel3.com)