Entity Relationship Alerts
Relationship alerts are the general version of the e-commerce Back-in-Stock trigger, but for any custom entity - seats, flights, classes, shows, appointments, B2B inventory. The idea is simple:
When a record in one catalog changes, notify every contact who is attached to that record through a second, related entity.
You declare a parent entity - the catalog whose records change (e.g.
concert_tickets or series) - and choose how the audience is found: either a
related opt-in list (a waitlist / "follow" entity) or the people who fired a
relevant event (watched / viewed). See Two ways to define the audience below.
When a parent record's watched field transitions, Joryio resolves that audience, and emits a per-user event you can use as a journey or campaign entry trigger - deduplicated per person so flapping values can't spam anyone.
Why two entities?
The built-in product Back-in-Stock feature is one click because the product catalog has a known shape (a stock field, a subscriber list). Custom entities are freeform - Joryio can't guess which field means "available" or which entity holds the waitlist - so you declare it once in the parent entity's settings.
For most e-commerce stores the product catalog is still the easy path. Reach for relationship alerts when your catalog isn't products, or when the audience is an explicit list rather than inferred browsing intent.
Two ways to define the audience
Before the config, the key choice - who gets notified:
- Behavioral (
audienceSource: "event") - the people who fired an intent event (e.g.Watched Episode,Viewed Flight) that references the record, within a lookback window. No follow/waitlist records to maintain - the audience is the behavior. This mirrors how product back-in-stock builds its audience from browsing events. Best for "notify everyone who watched / viewed." - Explicit (
audienceSource: "child_entity", default) - the rows of a related opt-in entity (a "notify me" waitlist, a "follow this show" list). Best when people deliberately subscribe, independent of any behavior.
You don't need a follow entity for the VOD case - see Example 2.
"Notify me" as an event vs. a waitlist entity
A common question: if a "Notify me" tap emits an event (Notify Me with a
product_id), do you still need a waitlist entity? Usually not - point event
mode at it (intentEvent: "Notify Me", intentRefProperty: "product_id") and
you're done.
The one thing that pulls you back to a child entity is durability. An event audience is bounded by two things:
- the lookback (
intentLookbackDays), and - event retention - your workspace's data-retention TTL eventually deletes the event, and once it's gone event mode can't see it.
Restock timing is open-ended - an item might return next week or in eight months. If the "notify me" event has aged out by the time it restocks, event mode misses that person; a waitlist row persists until you clear it. So:
- Short, predictable horizon (new episodes, quick restocks) → event mode, no entity.
- Open-ended horizon, or you want a manageable "notify once then remove" list → child waitlist entity.
Configuration
Set it on the parent entity. In the app: Entities -> edit the entity ->
Settings -> Alert people linked to a record - switch it on and the section
shows the field to watch, what counts as a change, the event to emit, the
cooldown, who gets the alert, and the "skip people who already acted" rule.
Each control maps one-to-one onto a key of settings.relationshipAlert, which
is what the API and the assistant set:
| Field | Meaning | Example |
|---|---|---|
enabled | Turn the alert on | true |
triggerMode | What counts as a fire on field (see below) | "restock" |
field | The parent field whose transition triggers the alert | "available" |
eventName | The per-user event to emit | "back_in_stock" |
cooldownDays | Per-(person, record) suppression window (default 7) | 7 |
audienceSource | "child_entity" (default) or "event" | "event" |
childEntity | (child mode) internal name of the waitlist/follows entity | "ticket_waitlist" |
childRefField | (child mode) field on child rows holding the parent record's id | "ticketId" |
intentEvent | (event mode) the intent event whose audience to notify | "Watched Episode" |
intentRefProperty | (event mode) event property holding the parent record's id | "series_id" |
intentLookbackDays | (event mode) how far back to look (default 90) | 90 |
minDropPct | (decrease mode) minimum drop, in percent of the old value, before it fires (default 5) | 5 |
Trigger modes
restock- a number goes from ≤ 0 to > 0 (back in stock). Default.increase- a number goes up (e.g. an episode count 8 → 9).changed- a value changes to a new, non-empty value (e.g. a "latest episode id").decrease- a positive number falls by at leastminDropPctpercent to a positive number (e.g. a hotel rate 200 → 180). The custom-catalog twin of the built-in price drop - see Example 3.
In child-entity mode the child entity must declare a User Link (which
field maps to a contact, by user id / external id / email / phone / attribute) -
that's how each row resolves to a person. In event mode the intent event's
user_id is the contact. A prior unknown value never fires (for the numeric
modes), so first-time populates don't false-alert.
Example 1 - Concert tickets back in stock
Goal: a show sells out; when tickets are released again, notify everyone who asked to be told.
-
Parent entity
concert_ticketswith a numeric fieldavailable. -
Child entity
ticket_waitlistwith fieldsticketId(the show/record it references) andemail. Set its User Link toemail. -
On
concert_tickets, set:{"relationshipAlert": {"enabled": true,"triggerMode": "restock","field": "available","childEntity": "ticket_waitlist","childRefField": "ticketId","eventName": "back_in_stock","cooldownDays": 30}} -
When someone taps "Notify me", your app/site adds a row to
ticket_waitlist:{ ticketId: "SHOW-A", email: "[email protected]" }. -
When
concert_tickets/SHOW-Aavailablegoes0 → 50, Joryio emits aback_in_stockevent for every waitlisted contact.
Build a one-step journey (or a triggered campaign) entered by back_in_stock.
Personalize it with the record's own fields, which ride along on the event:
{{ event.name }} tickets are back! Grab yours before they sell out again.
The event also carries event.record_id, event.new_value (the new stock), and
any scalar field on the record.
Example 2 - New episode of a series someone watches (VOD / Netflix-style)
Goal: when a new episode of a series drops, tell everyone who has been watching that series.
Do I need a "follows" entity, or can I just use a watch event? You can use
the watch event - and for VOD that's the natural choice. You already emit a
Watched Episode (or watch_series) event when someone plays an episode; point
the alert at that event and Joryio notifies everyone who watched the series
recently. No follow records to create or maintain. Use a follows entity only
if you want an explicit opt-in "follow this show" list that's separate from
watching.
Is it the same as restock or different? The audience side here uses
behavior instead of a waitlist (that's the audienceSource: "event" option),
and the trigger differs too: a new episode isn't a 0 → >0 stock change,
it's an episode count going up (or a "latest episode" id changing). Same
engine, two different knobs.
What you need - just one entity:
-
Parent entity
serieswith:- a display field like
title, - a numeric field
episodeCount(or a stringlatestEpisodeId). - No child entity.
- a display field like
-
On
series, set:{"relationshipAlert": {"enabled": true,"triggerMode": "increase","field": "episodeCount","audienceSource": "event","intentEvent": "Watched Episode","intentRefProperty": "series_id","intentLookbackDays": 90,"eventName": "new_episode","cooldownDays": 1}}(Prefer
"triggerMode": "changed"with"field": "latestEpisodeId"if you track the newest episode by id rather than a running count.) -
The connection is the event property. Your player already sends, on each play, something like:
{ "event": "Watched Episode", "series_id": "S-42", "episode": 12 }intentRefProperty: "series_id"is what links a watch to theseriesrecord- it must match the record's id or primary-key value (
S-42).
- it must match the record's id or primary-key value (
-
How to trigger: when your catalog ingestion bumps
series/S-42episodeCount8 → 9(a normal record update), Joryio queries who firedWatched Episodewithseries_id = S-42in the last 90 days and emits anew_episodeevent for each of them. -
Build a journey/campaign entered by
new_episode:New episode of {{ event.name }} is out now. Pick up where you left off →
If you prefer an explicit follow list instead of watch behavior, keep
triggerMode: "increase" but use the child-entity audience: add a
series_follows entity (seriesId, userId, User Link → userId), set
audienceSource: "child_entity", childEntity: "series_follows",
childRefField: "seriesId", and add a follow row when someone taps Follow.
Restock vs. new-episode at a glance
| Concert restock | New episode (VOD) | |
|---|---|---|
| Entities needed | parent + child waitlist | just the parent |
| Audience | ticket_waitlist rows (opt-in) | Watched Episode event (behavior) |
audienceSource | child_entity | event |
| Watched field | available (stock) | episodeCount / latestEpisodeId |
triggerMode | restock (0 → >0) | increase / changed |
| Emitted event | back_in_stock | new_episode |
Example 3 - "The price for your dates just dropped" (hotel rates)
Goal: a guest searched for a stay and saved the dates; when the rate for that room and those dates falls, tell them - the way the built-in price drop does for Shopify and WooCommerce products, but for a catalog Joryio does not sync.
-
Parent entity
hotel_rateswith a business primary keyrateId(one row per room type and date range), a display fieldhotel, and a numeric fieldrate. -
Audience - either a child entity
saved_searches(rateId,email, User Link →email) filled when the guest saves the dates, or event mode on theRate Viewedevent your booking site already sends with arate_id. -
On
hotel_rates, set:{"relationshipAlert": {"enabled": true,"triggerMode": "decrease","minDropPct": 5,"field": "rate","childEntity": "saved_searches","childRefField": "rateId","eventName": "rate_drop","cooldownDays": 3,"excludeEvent": "Booking Completed","excludeRefProperty": "rate_id"}} -
When your rate feed updates
hotel_rates/R-1rate200 → 180(a 10% drop, past the 5% bar), Joryio emits arate_dropevent for every guest attached to that rate - minus anyone who already booked it (excludeEvent). A200 → 195update (2.5%) does not fire; neither does a first-ever rate, a rate that rises, or a rate that goes to0(that is a sell-out, not a deal). -
Build a journey entered by
rate_drop. Besides the record's fields, the event carries the pair the drop was measured from:{{ event.hotel }} for your dates is now {{ event.new_value }} - down {{ event.drop_pct }}% from {{ event.old_value }}.
Properties of a decrease fire
| Property | Meaning |
|---|---|
old_value | The field's value before the update |
new_value | The field's value after the update |
drop_pct | The drop as a percentage of old_value, rounded to 1 decimal (10 for 200 → 180) |
drop_pct carries the same name and meaning as on the built-in price_drop
event, so a journey condition written for one reads the same on the other. The
value pair is old_value / new_value rather than old_price / new_price
because here the watched field can be anything numeric - a rate, a fee, a fare,
a wait time.
A further drop to a new value (180 → 160) is a distinct transition and re-alerts after the cooldown; the same value arriving again does not.
Excluding people who already acted
If you send some time after release, you probably don't want to nudge people who already watched the new episode (or already bought the restocked item). There are two layers, and you'll often use both:
1. Skip them at fire time - excludeEvent
The alert can drop, from the audience, anyone who already fired an event for this transition's new value. For the VOD example, add to the config:
{
"excludeEvent": "Watched Episode",
"excludeRefProperty": "series_id",
"excludeValueProperty": "episode",
"excludeLookbackDays": 30
}
Now when episode 9 drops, Joryio notifies people who watched the series but
subtracts anyone who already watched episode 9 (series_id = this series
and episode = the new value). excludeValueProperty is what pins it to the
specific new episode - for that to line up, the field's new value should match
the id your watch event carries (this is cleanest with triggerMode: "changed"
latestEpisodeId, or when episode numbers equal the count). Works for restock too - setexcludeEventto your purchase event to skip recent buyers.
2. Re-check at send time - the accurate answer for delayed sends
Fire-time exclusion only knows who watched up to the moment the alert fires. If you deliberately wait 2 days before sending, people will watch during those two days - and only a send-time check can catch them. Do it in the journey:
- Entry trigger:
new_episode. - Wait 2 days.
- Condition / behavior split: has NOT done
Watched Episodewhereepisode={{ trigger.new_value }}in the last 2 days → only this branch continues to the send.
This re-evaluates each person the moment before sending, so anyone who caught up
during the wait is filtered out. Use excludeEvent (layer 1) to keep the initial
audience tight, and the journey condition (layer 2) to make a delayed send exact.
Using the alert event
Whatever you name it, eventName behaves like any other event in Joryio:
- It appears in the event picker as a journey/campaign entry trigger.
- Its properties (
record_id,new_value,field, plus the record's scalar fields and anamelabel - and, for adecreasealert,old_valueanddrop_pct) are available asevent.*in Liquid. - Sends still honor consent, suppression, and quiet-time like every other channel send.
How it fires (under the hood)
The transition is detected on the entity update path and fanned out off the
hot path, so a slow audience never blocks the record write. Notifications are
deduplicated per (contact, record) for cooldownDays, and a child row only
counts once even if several rows resolve to the same contact.
Bulk imports
Bulk record imports don't fire alerts by default - so a first-time load never
pages everyone. Opt in per import by sending triggerAlerts: true on the bulk
endpoint. When enabled and the entity has a business primary key, the import
upserts-and-diffs: records that already exist are updated and their transitions
fire (e.g. a nightly stock or episodeCount feed), while brand-new rows only
fire changed-mode alerts (numeric modes need a prior value to transition from).
Getting set up
Three ways in, all writing the same relationshipAlert config:
- In the builder - open the parent entity (Entities -> edit), scroll to
Settings -> Alert people linked to a record, switch it on and fill in the
field, the mode, the event, the cooldown, the audience and the skip rule. The
builder only offers number fields for the numeric modes, shows the percent
input for
decreaseonly, and refuses to save an out-of-range percentage. A child waitlist entity still needs its Contact link set in its own settings. - Through the API - send
settings.relationshipAlerton the entity with the keys in the table above. The same rules apply: a knowntriggerModeand aminDropPctbetween 0 and 100. - With the AI assistant - ask for the whole thing in one request: it creates the child follows/waitlist entity, sets the config, and builds the notify journey.
Switching the alert off in the builder keeps the configuration with
enabled: false, so switching it back on later finds everything as it was.