Table of contents
Before a customer transaction can be applied to a loyalty challenge, it must be in the Adobe Loyalty Event format that the Challenge Service understands. Customer events — from a POS system, a mobile app, an e-commerce platform, or any other source — typically use the customer’s own data schema. Event Transformers bridge this gap without requiring any changes to the upstream system.
Overview
An Event Definition tells the platform two things:
- Which events to claim — how to recognize that an incoming event belongs to this definition (matching)
- How to reshape them — a JSONata expression that maps the customer’s fields to the Loyalty Event format (transformation)
Multiple event definitions can be configured per org. The platform evaluates them in order and applies the first one that matches. Events that don’t match any definition fall through to native ingestion (see Fallback — Native Loyalty Events).
The Adobe Loyalty Event Format
Every event definition must produce a JSON object in the following format. This is the input the Challenge Service processes.
{
"_id": "string — optional; used for duplicate detection if enabled",
"event_name": "string — used for internal metrics and reporting only (e.g. 'purchase', 'visit')",
"timestamp": "ISO 8601 date-time string — when the event occurred",
"utc_offset": "string — UTC offset of the store or device (e.g. '-07:00'); required for daypart matching",
"location_id": "string — optional; store or location identifier",
"transaction_id": "string — optional; dedup key for the transaction",
"loyalty_identity": {
"id": "string — the member's loyalty ID"
},
"item_list": [
{
"item_set": ["string", "..."], // one or more identifiers — SKU, category, event code, etc.
"item_name": "string — optional human-readable label",
"quantity": 1, // integer; how many units
"unit_price": 4.99, // float; price per unit
"sub_total": 4.99 // float; line total (quantity × unit_price)
}
]
}
Field Notes
loyalty_identityid — the member’s loyalty ID.item_listitem_settimestamputc_offset_idsub_totalEvent Definition Fields
guidname"Starbucks POS Purchase".xdmSchemaIdtransformerHow Matching Works
Events arriving through the Data Collection Core Service (DCCS) carry an XDM schema reference in their envelope. The platform reads the schema ID from /body/xdmMeta/schemaRef/id and compares it against each definition’s xdmSchemaId.
The platform walks the org’s event definitions in order and applies the first match. Once a match is found, the xdmEntity body is passed to the transformer.
Writing the Transformer
The transformer field is a JSONata expression. It receives the incoming event JSON as its input and must return a valid Adobe Loyalty Event object.
Map each top-level field of the target format to the corresponding path in your source event:
| code language-jsonata |
|---|
|
If all events matching this definition represent the same logical activity, hardcode the event_name:
| code language-jsonata |
|---|
|
event_name is used for internal metrics and reporting. It is not used as a task filter — task qualification is determined by item_set contents, not the event name.
For events arriving via the DCCS route, the member’s identity is typically carried in the standard XDM identityMap field rather than a custom tenant property. identityMap is a map keyed by namespace — the key itself is the namespace name, and the value is an array of identity objects.
| code language-jsonata |
|---|
|
-
Namespace substitution: Replace
Emailwith whatever namespace your org uses for loyalty members —Loyalty,ECID,CRMID, etc. Always read from the namespace that holds the primary loyalty profile identity. -
Always use
[0]:identityMap.Emailis an array. Without the index, JSONata returns a sequence rather than a single value if more than one identity is present, andloyalty_identity.idbecomes a list. Pin it to the first element with[0]. -
Avoid custom tenant fields for identity: Custom field groups sometimes expose an email-looking field (e.g.
_yourtenant.identification.core.email). In sample data this returns a value and looks correct, but in production events it is frequently empty. The reliable source of identity is alwaysidentityMap.
item_setitem_set is an array of string identifiers. Include every field that your challenge tasks might filter on:
| code language-jsonata |
|---|
|
For non-transactional events (a check-in, a survey completion, a custom trigger), a single identifier is sufficient:
| code language-jsonata |
|---|
|
unit_priceunit_price should be a per-unit price. Some source schemas store a line total (price × quantity) instead. If your source field is a line total, divide by quantity to get the unit price:
| code language-jsonata |
|---|
|
Only divide if your source field is a line total. If it already stores a per-unit price, map it directly — dividing a unit price by quantity will silently produce a wrong value.
transaction_idIf your source event doesn’t include a transaction identifier, you can derive a stable one from the timestamp:
| code language-jsonata |
|---|
|
This converts the ISO timestamp to epoch milliseconds and produces a deterministic value for a given event. Use your platform’s own ID generation function if one is available.
The full JSONata function library is available. Useful examples:
| code language-jsonata |
|---|
|
Examples
Scenario: A mobile app sends a check-in event. There are no line items — the event itself is the qualifying activity.
Incoming Event:
| code language-json |
|---|
|
Event Definition:
| code language-json |
|---|
|
Formatted Transformer (for readability):
| code language-jsonata |
|---|
|
Output Adobe Loyalty Event:
| code language-json |
|---|
|
A challenge task with no include/exclude restrictions will count this event as a qualifying visit — the single item_set entry ["store-checkin"] matches any task that allows all items.
Scenario: A point-of-sale system sends a transaction payload. Each line item has a SKU and belongs to a category. Challenge tasks use SKU and category to determine what qualifies.
Incoming Event:
| code language-json |
|---|
|
Event Definition:
| code language-json |
|---|
|
Formatted Transformer:
| code language-jsonata |
|---|
|
Output Adobe Loyalty Event:
| code language-json |
|---|
|
A challenge task with include: ["BEVERAGE"] would see the coffee line item qualify (its item_set contains "BEVERAGE") and accumulate $9.00 of spend toward that task. The muffin line item would be excluded.
Scenario: Events flow through Adobe Journey Optimizer. The incoming event is an XDM Experience Event with a known schema ID. The platform uses the schema ID for matching rather than a path/value check.
Incoming XDM Entity Body (the xdmEntity extracted from the AJO event):
| code language-json |
|---|
|
Event Definition:
| code language-json |
|---|
|
Formatted Transformer:
| code language-jsonata |
|---|
|
Note: When an event matches by XDM schema ID, the transformer receives only the
xdmEntityportion of the event — not the outer AJO envelope. All paths in your transformer expression are relative to the XDM entity body.
Adding JSON Schema Validation
The schema field is required. Set it to a JSON Schema document encoded as a JSON string to validate the structure of incoming events before transformation runs.
Events that fail schema validation are rejected before transformation runs. The error response includes the specific validation failure, making it easy to diagnose malformed upstream events.
| code language-json |
|---|
|
Pass this schema as a minified JSON string in the schema field of the event definition.
Fallback — Native Loyalty Events
If no event definition matches an incoming event, the platform attempts to ingest it directly as a native Adobe Loyalty Event. If the payload already conforms to the Loyalty Event format described above, no transformer is needed and the event is applied as-is. This allows customers who have pre-formatted their events to bypass transformation entirely.
API Reference
All event definition operations use the base path /loyalty/metadata/config/events.
| code language-http |
|---|
|
| code language-http |
|---|
|
| code language-http |
|---|
|
| code language-http |
|---|
|
Transformer Validation
JSONata expressions are validated for syntax when the event definition is saved. If the expression is invalid, the API returns a 422 error with a description of the parse failure.
To test a transformer before deploying, use the JSONata Exerciser — paste your source event as the input and your transformer expression to verify the output matches the expected Loyalty Event format.
Common Pitfalls
These mistakes all run without error on a simple single-item test payload, which is exactly why they slip through undetected. Always test your transformer against a payload with two or more products before deploying.
The most frequent mistake. Using a single object literal with productListItems.SKU pulls every SKU and every quantity into lumped sequences rather than producing one line item per product.
✗ Collapses all items into one:
| code language-jsonata |
|---|
|
With two products, item_set holds both SKUs and quantity becomes an array like [1, 4].
✓ One line item per product:
| code language-jsonata |
|---|
|
The .{ } map runs once per product so each becomes its own entry.
identityMap.Email is an array. Without [0], if a profile has more than one identity in that namespace, id becomes a list of values instead of a single string.
✗ identityMap.Email.id
✓ identityMap.Email[0].id
_yourtenant.identification.core.email. In sample data it returns a value and looks correct, but in production events it is frequently empty, causing loyalty_identity.id to come out null. Always use identityMap as the source of identity.item_setAdding a category field to item_set looks straightforward, but if productCategories is itself an array the result expands unpredictably.
✗ May produce more entries than expected:
| code language-jsonata |
|---|
|
A product with three categories produces an item_set with four values.
✓ Index the nested array to get exactly one value:
| code language-jsonata |
|---|
|
item_list is empty or missingAn event with an empty or absent item_list is rejected as invalid. For non-transactional events (check-ins, custom triggers) there are no natural line items, so produce a synthetic one:
| code language-jsonata |
|---|
|
timestamp as a Unix epoch integer instead of ISO 8601The platform expects an ISO 8601 string. If your source event carries milliseconds since epoch, convert it:
| code language-jsonata |
|---|
|
utc_offset omittedutc_offset, daypart window matching and consecutive-day streak counting are both skipped. Map the store or device UTC offset from your source event wherever it is available.xdmEntity body — not the outer AJO envelope. All paths must be relative to the XDM entity root. If your expression references fields that live in the outer envelope (e.g. /body/xdmMeta/...) they will not be found and will silently produce null.