Bowmark AIdocs

lululemon

lululemon's athletic apparel catalogue — search it, and read one product's full configurator: every colourway with its own price and images, the size…

lululemon's athletic apparel catalogue — search it, and read one product's full configurator: every colourway with its own price and images, the size options, and which exact SKUs are buyable right now.

Domain: shop.lululemon.com

Also known as: lululemon, Lululemon, lululemon athletica, lulu lemon, LULU

Prefer the capability

bowmark.yoga_outfit_shopping covers this provider and routes around it when it is having a bad day. Reach for this page when you need shop.lululemon.com specifically.

Call it directly

bowmark.providers.lululemon.search(query: { query: string; limit?: number; offset?: number }): Promise<LululemonSearch>
bowmark.providers.lululemon.getProduct(query: { productId: string }): Promise<LululemonProduct>
bowmark.providers.lululemon.getProducts(query: { productIds: string[] }): Promise<LululemonProductBatch>
bowmark.providers.lululemon.getProductAttributes(query: { productId: string }): Promise<LululemonProductAttributes>
bowmark.providers.lululemon.getSimilarProducts(query: { productId: string; limit?: number }): Promise<LululemonSimilarProducts>

Functions

FunctionWhat it does
searchSearches lululemon's catalogue by free text and returns matching product rows, closest match first — id, title, URL, price range, how many colours the style comes in, and whether it is in…
getProductReads one product's full configurator the way its product page presents it — every colourway with its own price, sale price, promo message, swatch, image set and URL; the size picker…
getProductsReads the full configurator for MANY products in one call — the shape to use when ranking a candidate set, because a search row carries a price range and a colour count but not the…
getProductAttributesReads what lululemon's OWN product page publishes about a garment and the third-party pricing door does not carry at all: the category the site files it under, the collection description,…
getSimilarProductsReturns the products lululemon's own product pages recommend alongside one product — the 'You may also like' rail — as priced rows in the store's own ranked order, de-duplicated to one row…

Types

interface LululemonVariant {
  /** The identifier lululemon's own checkout uses, e.g. "us_117376359". */
  sku: string;
  /** Which option each dimension is set to, e.g. { size: "4" }. */
  options: Record<string, string>;
  /** The store's own availability flag for this exact SKU. */
  available: boolean;
  price: number | null;
  salePrice: number | null;
  /** Why this SKU's price is, or is not, a markdown. Derived from price and
   * salePrice, so it can never disagree with them. */
  sale: SaleEvidence;
}
interface SaleEvidence {
  /** True only with retailer evidence. NEVER from a low price or a title. */
  onSale: boolean;
  currentPrice: number | null;
  /** The list price, when the markdown is against one. */
  originalPrice: number | null;
  /** ISO 4217, or null. ALWAYS null here: this feed publishes bare numbers with
   * no currency code anywhere in the payload. */
  currency: string | null;
  promotionMessage: string | null;
  evidenceType: "compare_at_price" | "sale_price" | "retailer_sale_badge" | "published_promotion" | "none";
  /** The retailer text or field the classification rests on, verbatim. */
  evidenceText: string | null;
}
interface ProductImage {
  /** Absolute HTTPS url. */
  url: string;
  altText: string | null;
  /** The colourway's own colour — sound because lululemon publishes its image
   * list PER COLOURWAY, so every picture in it is that colour by construction. */
  color: string | null;
  colorId: string | null;
  /** Always [] here: this feed links a picture to a COLOUR, never to a size. */
  variantIds: string[];
}
/** The retailer's own labels. On this door that is audience and nothing else —
 * the HPDP feed publishes no description, tags, fabric, collection or category.
 * Every other field is null or [], honestly. */
interface PublishedProductAttributes {
  audience: string | null;
  garmentType: "sports_bra" | "tank" | "crop_top" | "leggings" | "shorts" | "other" | null;
  categories: string[];
  collections: string[];
  fabrics: string[];
  materials: string[];
  color: string | null;
  colorFamily: string | null;
  pattern: string | null;
  styleTags: string[];
  neckline: string | null;
  strapWidth: string | null;
  backDesign: string | null;
  sleeveLength: string | null;
  rise: string | null;
  waistband: string | null;
  inseam: string | null;
  legShape: string | null;
  fit: string | null;
  coverage: string | null;
}
/** Shared colour and family — what makes two pieces PLAUSIBLY coordinate. None
 * of it establishes a set. */
interface CoordinationMetadata {
  collectionNames: string[];
  fabricNames: string[];
  colorName: string | null;
  colorId: string | null;
  colorFamily: string | null;
  productFamily: string | null;
}
/** An EXPLICIT retailer-published relationship. ALWAYS [] on this door: the feed's
 * only product-to-product relation is the ALGORITHMIC similarity rail, which is
 * getSimilarProducts. Reading that as a set would turn "the recommender put these
 * near each other" into "lululemon sells these together". */
interface RetailerSetEvidence {
  evidenceType: "official_set" | "shop_the_set" | "complete_the_look" | "matching_piece";
  evidenceText: string | null;
  sourceUrl: string | null;
  setId: string | null;
  relatedProducts: Array<{ productId: string | null; handle: string | null; title: string | null; url: string | null }>;
}
interface LululemonOptionGroup {
  /** The machine name. "size" on every lululemon product measured. */
  type: string;
  /** The site's own label for the picker, e.g. "Size". */
  label: string;
  options: { value: string; label: string }[];
}
interface LululemonColorway {
  /** lululemon's own colour code, e.g. "TRUE-NAVY". */
  colorId: string;
  color: string;
  colorFamily: string | null;
  price: number | null;
  /** Set only when this colourway is marked down. */
  salePrice: number | null;
  promoMessage: string | null;
  /** The product URL pinned to this colour, as the store publishes it. */
  url: string;
  swatchImage: string | null;
  images: string[];
  /** The SAME pictures as images, carrying this colourway's colour and colorId.
   * images stays a bare string[] for existing callers; this is the additive half. */
  imageAssets: ProductImage[];
  sale: SaleEvidence;
  coordination: CoordinationMetadata;
  /** ISO 4217, or null. Always null — see SaleEvidence.currency. */
  currency: string | null;
  inStock: boolean;
  optionGroups: LululemonOptionGroup[];
  /** What can be BOUGHT in this colour right now — not the full size run. */
  variants: LululemonVariant[];
}
interface LululemonSizeType {
  /** A sibling product that is the same style in another length. */
  productId: string;
  size: string;
  selected: boolean;
}
interface LululemonProduct {
  id: string;
  title: string;
  brand: string;
  url: string;
  /** The store's own audience attribute, e.g. "women". */
  gender: string | null;
  rating: number | null;
  reviewCount: number | null;
  inStock: boolean;
  priceLow: number | null;
  priceHigh: number | null;
  colorways: LululemonColorway[];
  /** Other lengths of the same style. Empty on every product measured — on this
   * site an inseam is its OWN product, not an option. */
  sizeTypes: LululemonSizeType[];
  attributes: PublishedProductAttributes;
  coordination: CoordinationMetadata;
  retailerSetEvidence: RetailerSetEvidence[];
}
/** One published product-detail block from the page, verbatim. */
interface LululemonFeature {
  heading: string;
  body: string;
}
/** What lululemon's OWN product page publishes about a garment, which the
 * third-party pricing door does not carry at all. */
interface LululemonProductAttributes {
  productId: string;
  url: string;
  title: string;
  /** The site's own ProductGroup category, e.g. "Leggings". */
  category: string | null;
  description: string | null;
  /** Trademarked fabric names off the detail accordion, e.g. ["Nulu"]. */
  fabrics: string[];
  fit: string | null;
  /** "High-Rise" / "Mid-Rise" / "Low-Rise", as the title spells it. */
  rise: string | null;
  /** Every detail block, unmapped and in page order. */
  features: LululemonFeature[];
  /** The site's own aggregate — the honest one. The pricing door reports 0
   * reviews on products whose live page shows 22,748. */
  ratingValue: number | null;
  reviewCount: number | null;
  /** Per-FIELD origin for the eight facts above, keyed by the same names. THIS
   * is how a refused read is told apart from a garment with nothing published:
   * fabrics [] beside status "absent" is lululemon saying it names no fabric,
   * and fabrics [] beside status "unreachable" is the page refusing us. Ranking
   * that treats the two alike silently prefers whichever candidates loaded. */
  provenance: Record<string, FieldProvenance>;
  /** The roll-up. On a refused page ratio is 0 and unreachableFields names all
   * eight — the machine-readable form of the sentence in warnings. */
  completeness: Completeness;
  /** What could not be reached. Non-empty means the page refused. */
  warnings: string[];
}
/** Where one field's value came from, and the retailer text behind it. */
interface FieldProvenance {
  /** "published" — lululemon stated it. "absent" — the page rendered and said
   * nothing about this field. "unreachable" — the page refused, so UNKNOWN. */
  status: "published" | "absent" | "unreachable";
  /** Which door: "lululemon_pdp_ldjson", "lululemon_pdp_accordion" or
   * "lululemon_pdp_title". Null when nothing filled it. */
  source: string | null;
  /** The retailer's own words the value rests on, for a field derived from
   * prose. Null for a field the site published as a typed value. */
  evidence: string | null;
  /** On "unreachable" only: what was refused. */
  detail?: string;
}
interface Completeness {
  fields: number;
  published: number;
  absent: number;
  unreachable: number;
  /** published / fields, to 3dp. */
  ratio: number;
  /** The fields that are UNKNOWN rather than known-empty. */
  unreachableFields: string[];
  sourcesUsed: string[];
}
/** What getProducts returns. PARTIAL by construction — the pricing catalogue
 * holds ~39% of the ids in lululemon's own sitemap, so ids it does not carry are
 * NAMED rather than silently dropped or thrown over. */
interface LululemonProductBatch {
  /** In the order the ids were passed, not the order they finished. */
  products: LululemonProduct[];
  /** Every id that did not read, with the catalogue's own sentence. */
  missing: Array<{ productId: string; detail: string }>;
  requested: number;
  warnings: string[];
}
interface LululemonRow {
  id: string;
  title: string;
  url: string;
  priceLow: number | null;
  priceHigh: number | null;
  colorCount: number | null;
  inStock: boolean | null;
  /** False when the row came from the site's product index and the catalogue
   * behind the prices does not carry it. id and url still work. */
  priced: boolean;
}
interface LululemonSearch {
  query: string;
  products: LululemonRow[];
  /** How many entries matched IN TOTAL, before the row cap — the size of the
   * thing you are paging through, not of this page. */
  matched: number;
  /** Where in the ranked match list this page started. */
  offset: number;
  /** The offset that reads the NEXT page, or null at the end of the list. Pass
   * it back verbatim — it is NOT offset + products.length, because a row that
   * was reached for and lost is dropped from products and named in warnings, so
   * a page of 24 can hand back 22 rows. */
  nextOffset: number | null;
  warnings: string[];
}
interface LululemonSimilarProducts {
  seedProductId: string;
  products: LululemonRow[];
  totalRanked: number | null;
  warnings: string[];
}

Examples

// The configurator: which colours does the Align pant come in, and which of
// them can be bought in a size 6 right now?
// NOTE the shape of the answer: a colourway lists ONLY the sizes it can
// currently sell, so presence is the stock signal. v.available was true on
// every SKU ever observed here — do not filter on it and conclude anything.
const p = await bowmark.providers.lululemon.getProduct({ productId: "prod2020012" });
const inSix = p.colorways
  .filter((c) => c.variants.some((v) => v.options.size === "6"))
  .map((c) => ({ color: c.color, price: c.salePrice ?? c.price }));
return { product: p.title, sellableSizes: p.colorways[0]?.optionGroups[0]?.options.map((o) => o.label), inSix };
// Search, then read the cheapest match in full.
const { products, matched, warnings } = await bowmark.providers.lululemon.search({ query: "align pant" });
const priced = products.filter((r) => r.priced).sort((a, b) => (a.priceLow ?? 0) - (b.priceLow ?? 0));
const best = priced[0];
if (!best) return { matched, warnings, note: "nothing matched with a price" };
return { matched, warnings, detail: await bowmark.providers.lululemon.getProduct({ productId: best.id }) };
yoga_outfit_shoppingCoordinated yoga outfit shopping — the capability this provider backs.
aloyogaAlso backs the same capability.
beyondyogaAlso backs the same capability.