Skip to main content

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:

FieldMeaningExample
enabledTurn the alert ontrue
triggerModeWhat counts as a fire on field (see below)"restock"
fieldThe parent field whose transition triggers the alert"available"
eventNameThe per-user event to emit"back_in_stock"
cooldownDaysPer-(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 least minDropPct percent 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.

  1. Parent entity concert_tickets with a numeric field available.

  2. Child entity ticket_waitlist with fields ticketId (the show/record it references) and email. Set its User Link to email.

  3. On concert_tickets, set:

    {
    "relationshipAlert": {
    "enabled": true,
    "triggerMode": "restock",
    "field": "available",
    "childEntity": "ticket_waitlist",
    "childRefField": "ticketId",
    "eventName": "back_in_stock",
    "cooldownDays": 30
    }
    }
  4. When someone taps "Notify me", your app/site adds a row to ticket_waitlist: { ticketId: "SHOW-A", email: "[email protected]" }.

  5. When concert_tickets/SHOW-A available goes 0 → 50, Joryio emits a back_in_stock event 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:

  1. Parent entity series with:

    • a display field like title,
    • a numeric field episodeCount (or a string latestEpisodeId).
    • No child entity.
  2. 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.)

  3. 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 the series record

    • it must match the record's id or primary-key value (S-42).
  4. How to trigger: when your catalog ingestion bumps series/S-42 episodeCount 8 → 9 (a normal record update), Joryio queries who fired Watched Episode with series_id = S-42 in the last 90 days and emits a new_episode event for each of them.

  5. 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 restockNew episode (VOD)
Entities neededparent + child waitlistjust the parent
Audienceticket_waitlist rows (opt-in)Watched Episode event (behavior)
audienceSourcechild_entityevent
Watched fieldavailable (stock)episodeCount / latestEpisodeId
triggerModerestock (0 → >0)increase / changed
Emitted eventback_in_stocknew_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.

  1. Parent entity hotel_rates with a business primary key rateId (one row per room type and date range), a display field hotel, and a numeric field rate.

  2. Audience - either a child entity saved_searches (rateId, email, User Link → email) filled when the guest saves the dates, or event mode on the Rate Viewed event your booking site already sends with a rate_id.

  3. 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"
    }
    }
  4. When your rate feed updates hotel_rates/R-1 rate 200 → 180 (a 10% drop, past the 5% bar), Joryio emits a rate_drop event for every guest attached to that rate - minus anyone who already booked it (excludeEvent). A 200 → 195 update (2.5%) does not fire; neither does a first-ever rate, a rate that rises, or a rate that goes to 0 (that is a sell-out, not a deal).

  5. 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

PropertyMeaning
old_valueThe field's value before the update
new_valueThe field's value after the update
drop_pctThe 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 - set excludeEvent to 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:

  1. Entry trigger: new_episode.
  2. Wait 2 days.
  3. Condition / behavior split: has NOT done Watched Episode where episode = {{ 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 a name label - and, for a decrease alert, old_value and drop_pct) are available as event.* 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 decrease only, 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.relationshipAlert on the entity with the keys in the table above. The same rules apply: a known triggerMode and a minDropPct between 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.