How tags work here
Tags are key–value strings on a user, shared by every subscription that user has. Set them from the SDK with addTag / addTags (the same call a OneSignal integration already makes), from the REST API on the user resource, or by a tag node inside a journey. A value that parses as a number compares numerically in segment filters, so game_level > 10 means what it says. There is no plan gate on the number of keys. Tags are included in every export and are deleted with the user.
Before you add a tag: is it already a field?
Several things teams tag out of habit are first-class on OpenPush and filter without a tag. Tagging them again only gives you two copies that drift.
| What you want to filter on | Use this instead of a tag |
|---|---|
| Signup date, account age | first_session — set when the device first registers |
| Last seen, lapsed players | last_session and session_count, updated by the SDK |
| Language | language, read from the device and overridable |
| Which build they are on, "updated the app" | app_version, plus device_type and country |
| Username, player id | The external id (an alias) — this is how you address one person from your server; it is an identity, not a tag |
| Clicked the last notification | Recorded per message already; branch on it inside a journey with a yes/no node on the notification action |
The schema
These keys cover almost every free-to-play game we have run. Numbers are numbers; flags are 1 / 0; dates are ISO days. "Set by" says who writes it — the app after the matching event, or the server (a journey tag node, or your backend through the REST key).
| Key | Type | Set by | Feeds |
|---|---|---|---|
| Progression | |||
game_level | number | app, on level up (only ever upwards) | Veterans (≥ 10) → the level-up reward journey; level-gated store pushes |
high_score · points · power | number | app, on the run or match ending | Leaderboard and tournament targeting; "you are 200 points from the next tier" |
character | string | app, on selection | Copy and image per character in one template with a {{tag}} substitution |
playstyle | string | app, from one onboarding question | Competitive / Explorers — different welcome journeys from day one |
| Economy and spend | |||
number_of_purchases | number | app or server, on purchase | Spenders (≥ 1); first-purchase journey exits when it changes |
ltv | number | server, on purchase (sum of amounts) | Whales (≥ 50) — never a real currency figure on a device you do not trust; use tiers if that worries you |
spender | flag | journey tag node after the first purchase, or the app | The cheapest segment filter you will ever write: spender = 1 |
last_purchase_at | ISO day | app or server, on purchase | Reporting and templates ("your last pack was …") |
days_since_purchase | number | app, on each session (today − last_purchase_at) | Lapsed spenders (spender = 1 and days_since_purchase > 30) → the win-back journey. See the elapsed-time note below. |
gem_balance | number | app, when the balance changes | "You can afford the hero now" pushes; a gem_balance < 50 segment for the refill offer |
vip_pass_purchased | flag | app or server, from the purchase SKU | Excluded segment on every pass upsell; included on pass-holder perks |
| Daily loops | |||
daily_gift_eligible | flag | app, at the daily reset (1) and on claim (0) | Gift unclaimed (= 1 and last_session older than 20 h) → one nudge, quiet hours respected |
daily_gift_claimed · daily_mission_achieved | ISO day | app, on the action | Streak copy; the retention journey's yes/no checks whether today's value is set |
store_visits | number | app, on opening the store | Browsers who never buy: store_visits ≥ 3 and spender = 0 |
| Onboarding | |||
completed_onboarding | flag | app, on finishing the tutorial — or the welcome journey's tag node | Excluded segment for every welcome message; the day-1 rescue journey targets = 0 |
onboarding_status | string | app, per step (started, step_3, done) | Where exactly they dropped out, for the rescue copy |
Numbers, days, and the elapsed-time trick
Segment filters compare a tag against a constant. That covers "level above 10" but not "purchased more than thirty days ago" — a constant cannot know today's date. Two ways round it, and both are cheap: keep a days_since_… number that the app recomputes on every session (one line of code, and a player who is not opening the app is exactly the one you filter on with last_session anyway), or hold the date in a …_at key for copy and reporting and let last_session carry the recency logic. Store dates as ISO days, not epoch seconds — a person reading the user page should not need a calculator.
What stays out
Anything that identifies a person outside your game — first name, birthday, email, phone, a real-world location — is linkable information even when it is not sensitive on its own, and belongs in a tag only if your privacy policy says you store it and why. OpenPush does not need any of it to send: the external id is the join key to your own systems, and templates can substitute a tag value without you ever storing a name. Never tag a secret, a session token, or a payment identifier. Tags leave with every export and show on the user page to everyone on your team.
Tags → segments → journeys
A tag earns its place by feeding a segment that feeds a journey. The pairs we run:
- New players (
first_sessionwithin 24 h,completed_onboarding≠ 1) → the welcome journey, which writescompleted_onboarding = 1when they tap through. - Lapsed 7d (
last_sessionolder than 7 d) → the comeback journey with a 10% holdout, exiting the moment a session is recorded. - Lapsed spenders (
spender = 1,days_since_purchase > 30) → the win-back offer. - Veterans (
game_level ≥ 10) → the level-up reward, with a yes/no onspenderdeciding which follow-up they get. - Gift unclaimed (
daily_gift_eligible = 1,last_sessionolder than 20 h) → a single nudge, sent at each device's own learned hour.
Coming from OneSignal
Your existing keys import unchanged and keep working in segments the same day — including the common ones (game_level, gem_balance, number_of_purchases, vip_pass_purchased, completed_onboarding, daily_gift_claimed, plan_type, subscription_status, trial_expiring, reward_redeemed, referral_invite, survey_response). Nothing needs renaming to match this page; the schema above is a recommendation, not a requirement. Two things are worth tidying after the import: drop tags that duplicate first-class fields (language, signup_date, updated_app), and check whether any key holds linkable information you would rather not carry across. The migration steps are on Migrate from OneSignal.
See it in the sample
The OpenPush.ai App has a "Try it" screen: level up, buy, open the store, finish the tutorial, claim the gift, complete a mission. Each tap records an event and writes the keys above on your own device, the sample segments update, and the demo journeys run against you with the waits compressed to seconds. Open the user page in the console while you tap and you can watch the tags change.