Skip to content

    Pre-launch API. The concepts described here are settled, but the API shape is not: request and response fields, parameters and defaults can still change. Build against it, and talk to your Luigi's Box contact before you put an integration into production.

    Rules and effects

    View source

    Each rule pairs a record_match — which products it applies to — with one effect.

    {
    "name": "Bury clearance stock",
    "record_match": {
    "operator": "and",
    "conditions": [
    { "field": "clearance", "operator": "eq", "value": true }
    ]
    },
    "effect": { "type": "bury", "strength_pct": 0.6, "mode": "hard" }
    }

    A rule’s name is required and is for you: it is what someone reads in six months when they find results they cannot explain.

    EffectWhat it doesFields
    pinPlace the product at a fixed positionposition (1-indexed), mode, decay_window, respect_user_filters
    banRemove the product from results entirely—
    boostRaise the product’s rankingstrength_pct (0–1), mode, decay_window
    buryLower the product’s rankingstrength_pct (0–1), mode, decay_window

    They divide into two kinds:

    • pin and ban are absolute. A pin puts a product at a position; a ban removes it. Both are decisions about the output rather than about how strongly a product scores.
    • boost and bury are relative. They change how strongly a product scores and let the results re-sort. Both are decisions about tendency.

    position is 1-indexed: 1 is the top slot. The product is inserted there and everything below shifts down — nothing is dropped.

    A pin naming one product places that product. A pin whose selector names several products — a list of IDs, or a criterion like brand eq "brand/northwear" — places whichever of them ranks highest, and several such pins fill their positions with distinct products. So “pin our best Northwear product to slot 1” is one rule, not a lookup you have to do yourself.

    A pin only ever moves a product up. A product that already ranks above the pin’s position is passed over: it needs no pin, and moving it down to the position would make the page worse. A pin naming several products then takes the next one down; a pin naming one places nothing, and the slot stays free for another rule to fill.

    So a pin is not a way to push a product down — that is what bury is for.

    In a campaign targeting one catalog, two pins that each name one product cannot share a position, or a product. A position holds one product and a product occupies one position, so only one of the pair could ever apply — the campaign is rejected rather than saved with the outcome left to chance.

    Two cases fall outside that. Pins naming several products, or a criterion, are not covered: which product each takes depends on how the results rank, so they cannot be said to collide until the search runs. Neither is a campaign targeting several catalogs, where a position is filled once per catalog — a different hero product pinned to slot 1 in each is two pins that never meet, and nothing in the rule says which catalog it was written for.

    A pinned product does not have to match the query. A campaign triggered by the query “wine” can pin a box of chocolates for Valentine’s Day: the product is fetched and placed even though the search would never return it. A pin naming one product has nothing to place when that product does not exist or a ban removes it. A pin selecting by criteria, or by a list of IDs, moves down its matches instead: a ban takes its top match out of the running and the next one takes the slot, so it places nothing only once no match is left.

    A pin places its product whether or not the filters in force would have kept it. That covers the shopper’s own facet selections, anything inferred from their query, and the scope of the collection being served: a pin on a chocolate places on the collection collection:category/wine/member:product.

    It does not cover the scope of the surface itself. A surface is configured to serve a particular part of your catalogue, and a pin cannot make it serve something else — that scope is your decision, not the campaign’s. Pin a product a surface excludes and the pin places nothing there.

    When the pin names several products, which one it places depends on those filters:

    1. A product the filters keep, if there is one — a pin on brand eq "brand/northwear" places a blue Northwear jacket for a shopper who filtered to blue.
    2. Otherwise a product that only contradicts the attributes the pin’s own selector names. The same pin, for a shopper who filtered to blue and to another brand, still places a blue Northwear jacket: the pin overrides the brand it names and leaves the colour alone.
    3. Otherwise its best product, filters aside.

    Relaxing is all or nothing at each step — a shopper filtered to size M who has no Northwear jacket in M is shown one in another size, not the nearest size.

    Three things a widening pin never places through. A ban always applies, so a banned product is never pinned. The object type the request asks for always applies, so no pin ever answers a request for articles with a product. And a surface collapsing on primary or variant serves one kind of group member, so a pin fills its slot from that kind — on a primary surface it places a primary, never a variant.

    A pin naming one product by ID is the exception to the last one. It names the product its author meant, so it takes its group’s slot even when that product is a variant and the surface collapses on primary. The object type still holds: an ID pin on an article places nothing on a request for products.

    Set "respect_user_filters": true on the effect to turn all of this off: the pin is then filtered like any other product, and places nothing when nothing it selects survives the filters. Use it when a product must not be shown past a filter: an age-restricted or regulated item the shopper has filtered away should stay away.

    Facets count everything the page matches (the query’s matches on a search, the whole collection on a collection page) plus every product a pin placed, so a pinned product’s values are counted like any other hit’s. Pin a chocolate onto the wine collection and the category facet lists both wine and chocolate, and the total counts the chocolate too.

    The shopper’s own filters are where a pin and the facets part. A pin ignores them, so it stays on the page after the shopper filters it away, but the facets follow the filters: the total and every facet on another field leave it out. Only the facet on the filtered field itself still includes it, as that facet always lists the values the shopper could switch to.

    strength_pct is a strength between 0 and 1, not a number of positions. How far a product actually moves depends on how tightly packed the results around it are, so a boost does not lift a product over results that match the query substantially better.

    Start around 0.3 for a nudge and 0.7 for a strong push, then look at the result.

    Boost and bury act on products the results already contain. A product that would not have been retrieved for this request is not boosted into it — a boost changes the order of an answer, it does not change what the answer is about. If you need a product to appear on a request it does not match, that is a pin.

    Boost and bury never cancel out: when both would apply to one product, one wins outright and the other is discarded. At equal specificity the bury wins — see Resolving conflicts.

    On a surface that recognizes product codes, a search for a code serves the products whose code matches it first, ordered by how strongly it matches. Boost, bury and pin act on that order like on any other: a buried matching product moves down, and a boost can lift another product above the matches. On a surface that collapses variants, a matching product can be represented by another member of its group.

    pin, boost and bury can fade. mode: "hard" (the default) applies the effect at full strength for the campaign’s whole run. mode: "soft" fades it out — full strength at the start, tapering to nothing. A ban is always hard: there is no half of a product removed.

    { "type": "boost", "strength_pct": 0.5, "mode": "soft", "decay_window": "P3D" }

    What fading means depends on the effect. A soft boost or bury weakens its score modifier until it stops moving the product at all. A soft pin loosens its hold on the position: the product starts at the slot the pin names and drifts back toward where it would have ranked on its own, instead of dropping back there in one step when the campaign ends.

    { "type": "pin", "position": 1, "mode": "soft", "decay_window": "P3D" }

    decay_window is an ISO 8601 duration: P3D is three days, PT12H twelve hours. It is only valid with mode: "soft", and must be positive. Leave it out and the fade lasts the whole campaign.

    The fade counts from starts_at and needs both starts_at and ends_at: a soft effect in a campaign without both bounds behaves like a hard one and never fades. Once the fade reaches zero the rule stops applying for the rest of the campaign.

    Soft mode is how you launch a new product hard and let it earn its own place, instead of ending a campaign with a cliff.

    A request that asks for an explicit sort is asking for its own order, so pin, boost and bury do not apply to it — there is no ranking left for them to change. Sort by price ascending and the cheapest product is first, whatever any campaign says.

    The permission a pin carries goes with it. A pin normally places its product even when the shopper’s filters exclude it; under a sort there is no placement, so that permission is not granted either, and the product is filtered exactly like any other. A shopper who filters to blue and sorts by price sees no red one.

    ban is the exception. It is enforced while the results are retrieved rather than while they are ranked, so a banned product stays off a sorted page too.

    Pins are placed after scores are adjusted, so a pin overrides any movement a boost gave the same product. A product boosted to position 2 and then pinned to 1 ends up at 1 — and a product boosted upward while another product is pinned above it lands one slot lower than the boost alone would suggest.

    Pin for a position, boost for a tendency. When both apply to one product, the pin decides.

    record_match uses the same condition syntax as a trigger, over catalog fields rather than trigger. fields. It selects by identity or by criteria, and the difference decides who wins a conflict:

    By identity — the most specific kind of rule:

    { "field": "@id", "operator": "in", "value": ["product/sku-4410", "product/sku-4411"] }

    Each value is the object’s full @id, type included, exactly as the catalog returns it. A bare sku-4410 names no object, so a rule on it could never act on anything, and the save refuses it.

    By criteria — matches whatever fits, now and later:

    { "field": "brand", "operator": "eq", "value": "brand/northwear" }

    Prefer criteria. A rule on clearance = true keeps meaning the right thing as stock turns over; a list of IDs has to be maintained as stock changes.

    “Product” here means any catalog object the surface serves. The same selectors and effects apply to categories, brands or articles, and the ID carries the type: category/shoes works like product/sku-1001.

    For a pin, a list of several IDs counts as criteria, not identity: a pin claims one position, so several IDs mean “whichever of these ranks highest”. Pin a single product when it must win a conflict on specificity.

    Operators follow the field’s type: gt, gte, lt, lte and range need a numeric field; eq, neq, in, not_in and exists work on any field, as long as the literal matches that field’s type. Break either rule — an operator the type does not support, or a literal of the wrong type — and the save fails with 422. matches is not available in record_match at all.

    A selector is a little more permissive than a trigger, so read the operator table as the trigger’s set rather than this one’s: membership works on a boolean here, where a trigger admits only eq, neq and exists. {"field": "clearance", "operator": "in", "value": [true]} is accepted in a selector and rejected in a trigger.

    A negative condition means has the attribute, and it differs — not the missing-attribute rule that applies everywhere else. Send {"field": "@category", "operator": "neq", "value": "category/shoes"} and it comes back with an exists on @category beside it, so products with no category at all are left out of the selector instead of banned along with the shoes. You can write that pair yourself; if you do, it is kept as you wrote it.

    A not around a single condition works the same way, from the outside: there it is the positive operators that fire on a missing attribute, so not (@category is shoes) is saved with an exists in front of it. A not around a whole group is left as written.

    To select the products that lack the attribute as well, write both halves out; see Keeping the original reading.

    For boost and bury, a selector with no conditions is a scope-wide effect — blunt, but coherent, and occasionally what you want. For pin and ban it is rejected with a 422, because it names no product: a pin would not say what to place, and a ban would not say what to remove.

    The check is about the selector being empty, not about what it matches. A ban whose conditions match every product, such as { "@id": { "exists": true } }, is accepted and removes everything the campaign reaches.

    A rule’s value can reference the trigger instead of a literal — “products whose brand is the same as the anchor’s brand”:

    {
    "field": "brand",
    "operator": "eq",
    "value": { "trigger_ref": "trigger.brand" }
    }

    That is one rule that works for every brand, instead of one rule per brand.

    Two rules bind the reference. The operator has to match its cardinality: a reference resolving to a single value takes eq or neq, one resolving to a list takes in or not_in. And it has to name the same field the condition is on — brand can only be compared with trigger.brand. Comparing one attribute against a different one is a condition that can never be true, so the save is rejected rather than leaving you with a rule that quietly matches nothing.

    On a search request there is no anchor to read the attribute from, so a rule referencing trigger.brand matches no product there.

    trigger.request:anchor_id is the exception, and may be compared with any field. It is how a rule picks out the anchor itself — @id equal to the anchor’s id, so a recommender can rule its own anchor out of the results.

    For a list-valued trigger attribute, pick which elements to use with path and select — first, last, all, first_n, last_n, at_index, all_but_first, all_but_last. first_n and last_n need an n; at_index needs an index. The default is the first element of the outer list and all elements within it.

    • Resolving conflicts — who wins when rules collide
    • Triggers — the condition syntax, and which requests fire
    • Filters — the operator vocabulary and the missing-attribute rule