Gamified quest with personalized reward offer loop
Modern engagement strategies go beyond static campaigns. By combining gamification with real-time personalization, you can create experiences that react to user behavior instantly — rewarding action, adapting to inaction, and delivering the right incentive to the right person at the right moment.
This use case describes a gamified in-app flow where a user completes a quest (for example, browsing 3 product categories). Upon completion, the system immediately evaluates the user’s profile — their spending history and recency of activity — to assign them to a segment (VIP, at-risk, or standard). A personalized offer with a time-limited voucher is then presented directly in the app.
If the user dismisses the offer or does not interact before it expires, the system sets a profile attribute and a workflow sends a mobile push notification after a delay, bringing the user back to the app where a modified offer with a higher discount is presented. Different users receive different rewards and timeout windows, even though the entire logic runs within a single flow.
The key value of this approach is event-driven orchestration in one place using Brickworks. All segmentation logic, voucher pool selection, offer copy, and escalation rules are defined within a single schema — eliminating the need to chain multiple campaign tools or maintain separate backend logic.
Prerequisites
- Implement Synerise SDK in your mobile app.
- Implement transaction events using SDK or API.
- Create voucher pools for each customer segment:
- A voucher pool for VIP customers (for example, 20–25% discount on premium items).
- A voucher pool for at-risk customers (for example, 30–40% discount).
- A voucher pool for standard customers (for example, 10–15% discount).
- Configure Google Firebase for mobile push notifications.
Process
In this use case, you will go through the following steps:
- Create aggregates to collect customer spending and transaction recency data.
- Create expressions that reference the aggregates to calculate customer spend and inactivity.
- Create a Brickworks schema with the offer logic.
- Create an in-app campaign with the gamified quest and reward flow.
- Create a workflow that sends a mobile push after the user dismisses or ignores the offer.
Create aggregates
In this part of the process, you will create two aggregates that collect the data needed for customer segmentation. These aggregates will be referenced by the expressions in the next part of the process.
Aggregate for lifetime transaction sum
This aggregate calculates the total value of all transactions made by a customer over their lifetime.
- Go to
Behavioral Data Hub > Live Aggregates > Create aggregate. - As the aggregate type, select Profile.
- Enter the name of the aggregate, for example
Sum of transactions lifetime. - Click Analyze profiles by and select Sum.
- From the Choose event dropdown list, select the
transaction.chargeevent. - As the event parameter, select
$totalAmount. - Set the time range to Lifetime.
- Save the aggregate.
Aggregate for last transaction timestamp
This aggregate returns the timestamp of the customer’s most recent transaction where revenue was greater than zero.
- Go to
Behavioral Data Hub > Live Aggregates > Create aggregate. - As the aggregate type, select Profile.
- Enter the name of the aggregate, for example
Last transaction time. - Click Analyze profiles by and select Last.
- From the Choose event dropdown list, select the
transaction.chargeevent. - As the event parameter, select
TIMESTAMP. - Click the + where button. From the Choose parameter dropdown list, select
$revenue. From the Choose operator dropdown, select More than (Number). In the value field, enter0. - Set the time range to Last 30 days.
- Save the aggregate.
Create expressions
In this part of the process, you will create two expressions that reference the aggregates created in the previous step. These expressions are later used inside the Brickworks schema to determine which segment a customer belongs to.
Expression for total spend
This expression returns the result of the lifetime transaction sum aggregate.
- Go to
Behavioral Data Hub > Expressions > New expression. - Enter the name of the expression, for example
Total customer spend. - In the Expressions for dropdown, select Attribute.
- In the Formula definition area, add the aggregate created in the previous step (
Sum of transactions lifetime). - Set the expression Type to Profile.
- Click Publish.
Expression for last transaction time
This expression returns the result of the last transaction timestamp aggregate.
- Go to
Behavioral Data Hub > Expressions > New expression. - Enter the name of the expression, for example
Last transaction time. - In the Expressions for dropdown, select Attribute.
- In the Formula definition area, add the aggregate created in the previous step (
Last transaction time). - Set the expression Type to Profile.
- Click Publish.
Create a Brickworks schema
In this part of the process, you will create a Brickworks schema that contains all the personalization logic. The schema uses expressions and Jinjava logic to determine the customer’s segment, select the appropriate voucher pool, and output a structured JSON payload consumed by the in-app message.
The schema checks the questOfferShown profile attribute to determine whether the customer has already seen the initial offer. If the attribute exists, the schema returns the modified offer with a higher discount. Otherwise, it returns the initial offer. The questOfferShown attribute is set from the in-app JavaScript code when the user dismisses or ignores the offer — it does not need to be created manually in advance.
The voucher is not assigned at schema level — instead, the schema returns the voucher pool UUID so the mobile app can handle voucher assignment directly. The voucher pool selection also differs between the initial and modified offer, allowing you to use separate pools for each stage if needed.
Segmentation logic
The Jinjava code inside the schema evaluates two data points:
- Spend (from the total spend expression): customers who spent more than 200 and were active in the last 14 days are classified as VIP.
- Inactivity (from the last activity expression): customers inactive for more than 14 days are classified as at-risk.
- All other customers are classified as standard.
Offer configuration per segment
| Segment | Initial offer | Timeout | Modified offer |
|---|---|---|---|
| VIP | 20% off premium items | 30 seconds | 25% VIP flash deal |
| At-risk | 30% flash comeback deal | 5 seconds | 40% emergency deal |
| Standard | 10% off + 2x loyalty points | 15 seconds | 15% off + 2x points |
Schema creation steps
- Go to Data Modeling Hub > Brickworks > New schema.
- Choose Singleton.
- Enter a name for the schema, for example
Quest reward offer.
Add the Offer field
- Click Add new field and choose Jinjava code.
- Complete the fields:
- In the Display name field, enter
Offer. - The API name will be pre-filled automatically.
- In the Display name field, enter
- In the Cast to field, select
JSON. - In the Jinjava code editor, paste the following code:
{%- set spendArr = [] -%}
{%- set inactiveArr =[] -%}
{%- expressionvar EXPRESSION_ID_SPEND -%}
{%- do spendArr.append(expression_result) -%}
{%- endexpressionvar -%}
{%- expressionvar EXPRESSION_ID_INACTIVITY -%}
{%- do inactiveArr.append(expression_result) -%}
{%- endexpressionvar -%}
{%- set spend = spendArr[0] -%}
{%- set inactive = inactiveArr[0] -%}
{%- set now_dt = timestamp|timestamp_to_time -%}
{%- set event_dt = '' -%}
{%- if inactive == 'null' -%}
{%- set event_dt = timestamp|timestamp_to_time -%}
{% else %}
{%- set event_dt = inactive|iso8601_to_time -%}
{%- endif -%}
{%- set now_day = now_dt|datetimeformat('%Y')|int * 365 + now_dt|datetimeformat('%j')|int -%}
{%- set event_day = event_dt|datetimeformat('%Y','UTC')|int * 365 + event_dt|datetimeformat('%j','UTC')|int -%}
{%- set days = now_day - event_day -%}
{% set seg = '' %}
{%- if spend > 200 and days < 14 -%}{% set seg = 'vip'%}
{%- elif days > 14 -%}{% set seg = 'atrisk'%}
{%- else -%}{% set seg = 'standard'%}
{%- endif -%}
{%- set isMutated = [] -%}
{%- if customer.questOfferShown -%}
{% do isMutated.append(true)%}
{%- else -%}
{% do isMutated.append(false)%}
{%-endif-%}
{%- set mutated = isMutated[0] -%}
{#- Resolve voucher pool UUID per segment and mutation state -#}
{%- set code = [] -%}
{%- if mutated -%}
{%- if seg == "vip" -%}
{% do code.append('VIP_VOUCHER_POOL_UUID') %}
{%- elif seg == "atrisk" -%}
{% do code.append('ATRISK_VOUCHER_POOL_UUID') %}
{%- else -%}
{% do code.append('STANDARD_VOUCHER_POOL_UUID') %}
{%- endif -%}
{%- else -%}
{%- if seg == "vip" -%}
{% do code.append('VIP_VOUCHER_POOL_UUID') %}
{%- elif seg == "atrisk" -%}
{% do code.append('ATRISK_VOUCHER_POOL_UUID') %}
{%- else -%}
{% do code.append('STANDARD_VOUCHER_POOL_UUID') %}
{%- endif -%}
{%-endif-%}
{%- if seg == "vip" -%}
{%- if mutated -%}
{%- set title = "25% VIP flash deal" -%}
{%- set desc = "We've upgraded your offer! 25% off premium items." -%}
{%- set discount = 25 -%}
{%- set cta = "Claim 25% off" -%}
{%- else -%}
{%- set title = "20% Exclusive deal" -%}
{%- set desc = "As a VIP member, you've unlocked an exclusive discount on premium items." -%}
{%- set discount = 20 -%}
{%- set cta = "Claim 20% off" -%}
{%- endif -%}
{%- set timeout = 30 -%}
{%- elif seg == "atrisk" -%}
{%- if mutated -%}
{%- set title = "40% emergency deal" -%}
{%- set desc = "Final offer: 40% off anything. This won't come back." -%}
{%- set discount = 40 -%}
{%- set cta = "Claim 40% off NOW" -%}
{%- else -%}
{%- set title = "Flash 30% off" -%}
{%- set desc = "Welcome back! Here's a special comeback deal. Don't let it slip away." -%}
{%- set discount = 30 -%}
{%- set cta = "Grab 30% off now" -%}
{%- endif -%}
{%- set timeout = 5 -%}
{%- else -%}
{%- if mutated -%}
{%- set title = "15% off + 2x points" -%}
{%- set desc = "We've boosted your offer! 15% off and double loyalty points." -%}
{%- set discount = 15 -%}
{%- set cta = "Claim 15% off" -%}
{%- else -%}
{%- set title = "10% off + 2x points" -%}
{%- set desc = "Great job! Enjoy a 10% coupon and double loyalty points on your next order." -%}
{%- set discount = 10 -%}
{%- set cta = "Claim reward" -%}
{%- endif -%}
{%- set timeout = 15 -%}
{%- endif -%}
{
"segment": "{{ seg }}",
"offerTitle": "{{ title }}",
"offerDescription": "{{ desc }}",
"discountValue": {{ discount }},
"ctaLabel": "{{ cta }}",
"offerTimeout": {{ timeout }},
"offerStyle": "{{ seg }}",
"voucherCode": "{{ code[0] }}",
"isMutated": {{ mutated }}
}
Replace the following placeholders with your actual IDs:
EXPRESSION_ID_SPEND— the ID of the total spend expression.EXPRESSION_ID_INACTIVITY— the ID of the last transaction time expression.VIP_VOUCHER_POOL_UUID,ATRISK_VOUCHER_POOL_UUID,STANDARD_VOUCHER_POOL_UUID— the UUIDs of the voucher pools for each segment. You can use different pool UUIDs for the initial and modified branches if needed.
voucherCode field. The mobile application is responsible for calling the voucher assignment API using this UUID when the user accepts the offer. The voucher pool resolution is split into two branches (mutated and non-mutated), so you can configure separate pools for each stage if your business logic requires it.- Click Apply to save the field.
Set up the Audience & Settings
- Click the Audience & Settings tab.
- In the Audience section, click Define.
- Choose the schema recipients, in this case, choose Everyone.
- Click Apply.
- In the upper-right corner, click Save.
Create an in-app campaign
In this part of the process, you will create an in-app campaign that renders the gamified quest interface. The in-app message uses JSON to call the Brickworks schema once and handle the quest and reward flow client-side.
- Go to
Experience Hub > In-app > Create new. - Enter the name of the in-app campaign, for example
Quest reward offer loop.
Define the audience
- In the Audience section, click Define.
- Choose Everyone or specify the audience according to your business needs.
- Click Apply.
Define content
In this part of the process, you will create the in-app message template containing the quest UI and reward overlay.
- In the Content section, click Define.
- Click Create message.
- Click + New template in the upper right corner.
- Choose Code editor.
HTML tab
The HTML defines the key structural elements required for the flow to work. The essential parts are:
- Quest area (
#mainArea) — dynamically rendered quest card with progress bar and claim button. - Reward overlay (
#overlay) — bottom sheet that displays the personalized offer, countdown timer, and action buttons.
<div id="app">
<div class="topbar">
<h1>Quest hub</h1>
<div class="streak-badge">
<span id="streakCount">-</span>
</div>
</div>
<div class="main" id="mainArea"></div>
</div>
<div class="overlay" id="overlay">
<div class="reward-sheet">
<div class="reward-icon" id="rewardIcon"></div>
<div class="reward-title" id="rewardTitle"></div>
<div class="reward-sub" id="rewardSub"></div>
<div class="reward-timer" id="rewardTimer"></div>
<button class="reward-cta" id="rewardCta">Claim</button>
<button class="reward-dismiss" id="rewardDismiss">Maybe later</button>
</div>
</div>CSS tab
Define styles for the quest card, progress bar, overlay, reward sheet, and buttons according to your brand guidelines. The key functional styles that must be present are:
.overlay— must be hidden by default and shown via a.showclass (for example, usingdisplay: none/display: flexor opacity transitions)..progress-barand.progress-fill— the fill element’swidthis set dynamically via inline styles in JavaScript..claim-btn[data-state="locked"]— should appear disabled;[data-state="ready"]should appear active and clickable.
All other visual styling (colors, fonts, spacing, animations) can be customized to match your brand.
JavaScript tab
The JavaScript manages the core flow. The critical part is the Brickworks integration at the top — the Jinjava block that calls the Singleton schema and injects the offer payload as a JavaScript constant. Since the schema type is Singleton, the same schema ID is used for both schemaId and recordId:
{% set offer = [] %}
{% brickworksgeneratevar schemaId=SCHEMA_ID recordId=SCHEMA_ID %}
{% do offer.append(brickworks_result.offer) %}
{% endbrickworksgeneratevar %}
const BW_OFFER = {{ offer[0] }};This produces a single JavaScript object at render time containing the full offer payload (segment, offerTitle, offerDescription, discountValue, ctaLabel, offerTimeout, voucherCode, isMutated). Whether this is the initial or escalated offer is determined automatically by the Brickworks schema based on the questOfferShown profile attribute.
The rest of the JavaScript implements the following flow logic:
-
Quest progress tracking — increments a counter on each user action. When the counter reaches the target (for example, 3 steps), the claim button becomes active.
-
Claim action — when the user taps the claim button, a
form.submitevent withfd:formType = questis fired and the offer fromBW_OFFERis presented in the overlay. -
Offer presentation — the overlay is populated with data from the Brickworks payload (title, description, CTA label) and a countdown timer is started using the
offerTimeoutvalue. -
Accept — the user taps the CTA button. The mobile app uses the voucher pool UUID from
BW_OFFER.voucherCodeto assign a voucher via the API. A success state is shown. -
Dismiss / Timeout — if the user taps “Maybe later” or the timer expires, an
offer.dismissedevent is fired and thequestOfferShownattribute is set totrueon the customer’s profile. This triggers the workflow that sends a mobile push notification after a delay.
SCHEMA_ID in the Jinjava block with the actual ID of the schema you created. For Singleton schemas, the same ID is used for both schemaId and recordId. You can find the ID in the URL when viewing the schema in the Synerise platform.- To continue the process of configuring the in-app campaign, click Next.
- Click Apply.
Select events that trigger the in-app message display
In this part of the process, you will define the event that triggers the display of the in-app message. The quest interface should appear when the user opens a specific screen or section in the app.
- In the Trigger events section, click Define.
- Select Add event and from the dropdown list, choose
screen.viewevent. - Click the + where button and select the appropriate parameter to target the quest hub screen according to your app structure.
- Click Apply.
Schedule the message and configure display settings
-
In the Schedule section, click Define and set the time when the message will be active.
-
In the Display Settings section, click Change.
-
Define the Delay display, Priority index and enable the Frequency limit toggle to manage how often the quest is shown. For example, display the quest a maximum of 1 time per day.
Note: You can additionally enable the Capping limit toggle to limit the total number of times the in-app message can be displayed to a user. -
Click Apply.
-
Optionally, define UTM parameters and additional parameters for your in-app campaign.
-
Click Activate.
Create a workflow
In this part of the process, you will create a workflow that sends a mobile push notification to users who dismissed the offer or let it time out. The push brings them back to the app, where the Brickworks schema — now reading questOfferShown = true on their profile — returns the escalated (mutated) offer automatically.
- Go to
Automation Hub > Workflows > New workflow. - Enter the name of the workflow, for example
Quest offer follow-up push.
Define the Profile Event trigger nodes
The workflow uses two trigger nodes connected via a Merge Paths node, so the push is sent regardless of whether the user completed the quest without claiming or dismissed the offer.
First trigger — quest completed without claim
- As the first node of the workflow, add Profile Event. In the configuration of the node:
2. From the Choose event dropdown menu, choose the
form.submitevent. 3. Click the + where button. From the Choose parameter dropdown menu, choosefd:formType. From the Choose operator dropdown, choose Equal. In the value field, enterquest. - Confirm by clicking Apply.
Second trigger — offer dismissed
- Add a second Profile Event node. In the configuration of the node:
- From the Choose event dropdown menu, choose the
offer.dismissedevent. - Confirm by clicking Apply.
Configure the Delay node
- Add the Merge Paths node and connect both Profile Event trigger nodes to it.
- Add the Delay node. Configure the delay duration according to your business needs (for example, 15 minutes). This gives the user time before receiving the follow-up push.
- Confirm by clicking Apply.
Configure the Send Mobile Push node
- Add the Send Mobile Push node. In the configuration of the node:
- Define the push notification content. For example:
- Title:
Look at your new offer - Body:
We have a new deal for you
- Title:
- Optionally, enable the Send without marketing agreement option if your business requirements allow it.
- Define the push notification content. For example:
- Confirm by clicking Apply.
Add the finishing node
- Add the End node.
- In the upper right corner, click Save & Run.
Summary
The following describes the end-to-end flow from the user’s perspective and how the system components interact:
-
Quest phase: The user sees a quest card (for example, “Browse 3 product categories”). As they complete each step, the progress bar advances. The quest progress is tracked client-side in the in-app message.
-
Claim phase: When all steps are completed, the “Claim your reward” button becomes active. Tapping it fires a
form.submitevent withfd:formType = questand triggers the offer presentation. -
Initial offer: The in-app reads the
BW_OFFERpayload (generated from the Brickworks schema). Since thequestOfferShownattribute does not exist yet on the profile, the schema returns the initial offer. A bottom sheet overlay appears showing the personalized offer with the segment-appropriate discount, copy, and a countdown timer. -
User interaction paths:
- Accept: The user taps the CTA button. The app uses the voucher pool UUID from the payload to assign a voucher via the API, and shows a success confirmation.
- Dismiss / Timeout: The
offer.dismissedevent is fired and thequestOfferShownattribute is set totrueon the profile. The overlay closes.
-
Follow-up push: The workflow detects either the
form.submitoroffer.dismissedevent and, after the configured delay, sends a mobile push notification encouraging the user to return to the app. -
Escalated offer: When the user opens the app via the push, the in-app campaign triggers again. This time, the Brickworks schema reads
questOfferShown = trueon the profile and returns the modified offer with a higher discount.
Check the use case set up on the Synerise Demo workspace
You can check the configuration of each step directly in the Synerise Demo workspace:
- Aggregate — Sum of transactions lifetime
- Aggregate — Last transaction time
- Expression — Total customer spend
- Expression — Last transaction time
- Brickworks schema
- In-app campaign
- Workflow — Quest offer follow-up push
If you’re our partner or client, you already have automatic access to the Synerise Demo workspace (1590), where you can explore all the configured elements of this use case and copy them to your workspace.
If you’re not a partner or client yet, we encourage you to fill out the contact form to schedule a meeting with our representatives. They’ll be happy to show you how our demo works and discuss how you can apply this use case in your business.