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.
A campaign’s match decides whether it applies to a given request. It is a condition tree,
and every leaf field carries a trigger. prefix.
Search requests whose query contains “shoes”:
{ "operator": "and", "conditions": [ { "field": "trigger.request:query", "operator": "matches", "value": "shoes" } ]}Recommendations anchored on one product:
{ "operator": "and", "conditions": [ { "field": "trigger.request:anchor_id", "operator": "eq", "value": "product/sku-1001" } ]}Collection pages of one category, for a collection registered with membership_attribute
lbx:category_ids, in a campaign whose intents targeting is ["collection"]:
{ "operator": "and", "conditions": [ { "field": "trigger.lbx:category_ids", "operator": "eq", "value": "category/shirts" } ]}Without that targeting, the same condition also fires on recommendations anchored on a product in that category.
The prefix is what distinguishes this tree from a rule’s record_match: trigger. fields
describe the request, unprefixed fields describe a product.
What you can trigger on
Section titled “What you can trigger on”Fields from the request itself
Section titled “Fields from the request itself”Three fields, under the reserved request: namespace:
| Field | Value |
|---|---|
trigger.request:query | The search query. matches fires when the value appears in the query as whole words, in order: gift card fires on “gift card for men”, not on “card gift”, and sho does not fire on “shoes”. Case and accents are ignored. The comparison is on analyzed words, not raw text, so the catalog’s language rules apply first — shoe can fire on “shoes”, and a word the language treats as filler is dropped from both sides before the order is checked. |
trigger.request:type | The requested object type, e.g. product. Present only when the request is scoped to a single type. |
trigger.request:anchor_id | A recommender anchor’s ID in type/value form, e.g. product/sku-1001. A value without its type is refused on save: anchors are always sent typed, so it could never fire |
eq, neq, in and not_in on trigger.request:query compare the whole query,
case-insensitively. Any other trigger.request:* name is rejected with 422, and so is a
trigger_ref value: a trigger compares against literals only. A match with no conditions
fires on every request in the targeting scope.
No trigger field names the kind of request. To limit a campaign to search,
recommendations or collections, set its
intents targeting, or target only surfaces of
that kind; a trigger.request:intent condition is refused with 422
(filter_unknown_field), pointing there. A trigger cannot name a kind, so a condition that can fire
on several of the kinds a campaign targets fires on all of them. To give each kind its own
condition, use one campaign per kind.
Fields from the object the request is about
Section titled “Fields from the object the request is about”Everything else is a catalog attribute of the object the request is about — the recommender’s anchor, or a membership collection’s membership attribute (below):
{ "field": "trigger.brand", "operator": "eq", "value": "brand/northwear" }That fires the campaign on any recommendation anchored on a Northwear product, or on
collection:brand/northwear/member:product when the brand collection’s membership attribute
is brand. It says nothing about which products the rules then
affect — that is record_match.
A recommendation with several anchors fires the campaign when any one of the first ten
anchors satisfies the conditions. A membership collection exposes one attribute: its
membership attribute, holding the @id the
request names. A curated collection exposes each eq and in condition that its filters
join with and: { "label": { "eq": "new" } } fires trigger.label eq new. An all
collection exposes nothing, so no positive condition on a catalog attribute can fire on it —
what a negative one does is the next paragraph. The shopper’s own filters never trigger a
campaign.
A search request has no such object. A positive condition on a catalog attribute (eq,
in, a comparison) therefore never fires on search. A negative one (neq, not_in, or a
not group) fires on every search request, because the attribute is missing there and
a negative test on a missing attribute is true. The same holds for a recommendation with no
anchor and for a collection that exposes no attributes. If the attribute condition should
decide which products are affected rather than when the campaign fires, put it on the rule
instead.
Discovering what you can trigger on
Section titled “Discovering what you can trigger on”POST /platform/v1/campaigns/trigger-schema takes a targeting set and returns the trigger
fields authorable across it, with the exact operators each one accepts:
curl -X POST 'https://api.eu1.luigisbox.ai/platform/v1/campaigns/trigger-schema' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{"organization_id": "lbo_r2vn8c", "catalog_ids": ["lbc_8w3k2p"]}'Each entry carries a name (unprefixed — add trigger. to use it), a source of
catalog or request, the operators it accepts, and a value_domain saying whether its
values are a suggestion:
value_domain | Meaning |
|---|---|
suggested | What the scope carries today, but other values are accepted |
open | Free input; values is empty |
values is populated for trigger.request:type (suggested); the other two request fields
and every catalog attribute are open with an empty list. For a catalog attribute, read the
values from catalog metadata instead.
The response is paginated, and query
narrows it to a case-insensitive substring of the field name — a large catalog can carry a
lot of attributes.
Two reasons to call this endpoint rather than assume the field names:
- The operators it returns are the ones that will be accepted on save. If you are building an authoring UI, offer exactly these and a save can never be rejected on operator grounds. They also tell you which value input to render: range and comparison operators mean the field is numeric, membership-only means it is a keyword.
- It answers for the whole targeted scope, conservatively. A field that is numeric in one targeted catalog and text in another is reported with the keyword-safe operator set, because that is the set that will survive validation across every catalog in scope.
Condition syntax
Section titled “Condition syntax”The same syntax serves match and record_match. A condition tree is operator plus
conditions, nestable:
{ "operator": "and", "conditions": [ { "field": "brand", "operator": "eq", "value": "brand/northwear" }, { "operator": "or", "conditions": [ { "field": "color", "operator": "eq", "value": "blue" }, { "field": "color", "operator": "eq", "value": "navy" } ] } ]}Group operators are and, or, not. Leaf operators depend on the field’s type:
| Field | Operators |
|---|---|
| Text, and any field the catalog has no type for | eq, neq, in, not_in, exists |
| Numeric | the above plus gt, gte, lt, lte, range |
| Boolean | eq, neq, exists |
trigger.request:type, trigger.request:anchor_id | as text |
trigger.request:query | as text, plus matches |
matches works only on trigger.request:query: not on any other trigger field, and not in
a rule’s record_match. The trigger-schema endpoint
reports the exact set for each field.
A tree may hold up to 100 conditions in total, in groups nested up to 10 deep, and one
in or not_in condition may name up to 100 values. Past any of these, the save is
rejected; split a tree that large into several rules. Only conditions count, not the groups
holding them. Both limits apply to the tree as stored, so the presence checks added below
count towards them — usually one extra condition per negation, and a level when a negation
under an or is wrapped with its check in an and — and a rejection says when the pairing is
what put the tree over.
The evaluation rules are the same as discovery filters, with one
difference: a negation is stored with a presence check beside it, so it reads as “present,
and different” rather than firing on every request that happens to lack the field. Outside a
not that means neq and not_in; inside one it is the positive operators, since
not (brand neq nike) already means “has a brand, and it is nike”. @id, @type and
@updated_at are on every object and are left as written, as is a not around a whole
group. A negation on @title is paired like any other field’s: a rule does not depend on every
stored object carrying a title.
The three trigger.request:* fields are left as written too. A recommendation request carries
no trigger.request:query, so trigger.request:query neq "nike" does fire on every one of
them — but that is the request being of another kind, which you target through the
campaign’s targeting (intents, or surfaces of that kind) rather than through a presence
check. To keep a request-field negation off a kind, name the kinds the campaign serves.
To keep the original reading on a catalog attribute — “not nike, and brandless is fine” — write both halves out; see Keeping the original reading.
The tree is written differently, and campaigns accept two operators discovery does not —
matches and range. See
Other surfaces that filter.
See also
Section titled “See also”- Rules and effects — the other condition tree
- Targeting — where a campaign can act
- Catalog metadata — which attributes and values exist
Was this page helpful?
Thanks.