Audience, triggers, campaign, campaign_allocations
Audience and triggers are very intrinsic to aampe - not to be mixed with what we see or know from CRM world as of today.
Audience
Audiences are defined on attributes - what we call as an attribute is something that describes the contact(end user) itself. Things like age, language, country are attributes that don't need to aggregate / calculate and most likely don't have any notion of time window with them.
Triggers
Triggers are a different way of defining an audience based on events and time window. But internally we use attributes to calculate triggers as well - This can look confusing so lets look at it one thing at a time.
Understanding Attributes - Vanilla and time aggregated (rolled up)
Attributes (sometimes also referred to as properties) are what our customer shares with us about their contact.
But, other than attributes that come from data from customers directly, some could also be calculated(pre-computed) from events, these are also stored as attributes.
For example:
- number of purchase in last 7 days
event_name_purchase_completed-> eventday_7_event_name_purchase_completed-> pre computed by us and stored as attributes
- last seen day
- or any other pre computed property -
- some of this we compute as well.
Side note: Why pre-compute?
Think around adding a new dimension to these events - so purchase_completed event is already coming for contact_x, that tells us customers made a purchase - thats great. By aggregating it over time (new dimension) we get to know purchases made over last 7 days, 30 days etc.
Pre computation helps us with not having to do this look-back compute which will be costlier.
So we have vanilla attributes, attributes pre-computed over time, and anything else? Yes -
Snapshot attributes
These are attributes that keep the information on the type sort of like a pointer. For example:
days_since_first_seen
days_since_last_seen
last_time_zone
last_country
They are not aggregated over the time window technically but they have been maintaining a count since the first point/pointer.
Do we define audiences on all possible attributes?
This is what could help distinguish the audiences and triggers more:
Triggers are defined on event attributes over time window - Take an example:
- We want to create a bucket of contacts where number of purchases made in last 7 days > 0
- This would not be an audience, but a trigger in composer
- But to keep our logic on how we materialized this, we keep something called as
count_purchase_completed_7_days-- this is an attribute that we pre compute and keep. At the same time we also keep count of 1 day, 7 day, 30 day, all time. - when a condition is defined on an event with a time window we refer to it as trigger.
- But to keep our logic on how we materialized this, we keep something called as
Another example:
- We want to create a user who are in SG or speak english
- these are not time window attributes these would qualify as audiences.
Time window aggregated attributes:
days_1_*-> represents the aggregation of events in 1 daydays_7_*-> represents that we aggregate and keep for 7 day window
Anything that doesn't have this kind of prefix(days_*) is something that our customers sends to us directly and is not something we use for triggers but rather for defining audiences.
With the exception of:
days_since_first_seen
days_since_last_seen
we calculate and keep this as well -- but this is also used in audience. These are snapshot attributes.
TLDR on how do we use attributes to evaluate triggers definition
Lets say we want to send a message to anyone who has made a purchase in last 7 days:
- we count the number of purchases in last 7 days
- save this as an attribute (in
attributes_combinedBQ); maybe in a field likeday_7_event_name_purchase_completed- we also store more fields on 1 day 30 day and all time aggregation
day_1_event_name_purchase_completedday_30_event_name_purchase_completed- we may have stopped keeping all time aggregation - TBC with Saiyam
- we also store more fields on 1 day 30 day and all time aggregation
- and use it to define triggers in UI
- The event, condition and time window
- Event + time_window is pre-calculated and kept stored as attribute
- We then apply the condition on the stored value to see if the user is eligible or not
day_7_event_name_purchase_completed> 0, if yes then eligible for trigger message else no
Historical bite: why we built triggers and how is it still unique/different from conventional CRM "trigger"
Our customers wanted to target user based on events, for example contacts that have made a purchase in last 7 days. So to support this we ended up implementing this feature - triggers. We maybe could have implemented option in audience to define an audience based on events with time windows and then this whole idea of audience and triggers being two things could be avoided.
Usually in CRM when we say trigger or what they do based on conditional messaging is if there is an event it sorts of guarantees a message is sent to the user. Think of it something like - if the user has made a purchase > 7 days send them a "We are missing you text". This is like a rule based trigger that will execute to send a message always.
Aampe trigger is "eligibility" not a rule
It means if you satisfy the conditions in a trigger you qualify from eligibility but doesn't guarantee you get a message. You didn't make a purchase in last 7 days, so you qualify but Aampe may or may not send you a message.
Question then is - if you are eligible then for how long? Because aampe doesn't want to make you eligible at the instance of when you have done something but it wants to make you eligible for a window of time / range.
7 days - in trigger if a contact is eligible they are then eligible for 7 days (calendar days - no rolling window)
Why 7 days? why not 8, 14 or something else. For this we need to understand campaigns, after which triggers was built as a new feature.
Campaigns - the OG eligibility concept
When we started, we had campaigns, campaigns had an audience and messages attached to it. Campaigns were just a shell attaching messages to audiences. Every time a user enrolled, they were enrolled for a period of a week, and they would get message slots during that week.
The system would evaluate: "Is this contact part of this audience? Do they have slots assigned?" This weekly enrollment period is why we use 7 days in our triggers - it's based on our original campaign allocation design.
The campaign allocation process would wake up daily to check:
- If a contact is still part of the audience
- If they have assigned slots
- If their allocation has expired
This allocation system would:
- Keep your allocation if you still fulfill the conditions
- Remove you from the allocation if you no longer fulfill the conditions
- Give you a new allocation if you fulfill conditions for different audiences/campaigns
This is the foundation of how our campaign allocations still work today, though we've built additional functionality on top of it.
Is eligibility calculation a rolling window?
If the campaign allocation looks at eligibility every day how does this scenario play out:
- Day 0(T): Campaign allocation checks and makes a contact eligible for next week (till T+7) - picks up 3 slots based on frequency
- Day 1: Campaign allocation checks and makes a contact eligible for next week (till T+1 to T+8)? - will it pick additional slots - is it rolling window basically?
- Day 2: One slot was used to send message, now campaign allocation will it pick up new slots since there is only 2 slots configured but the frequency may allow more?
All eligibility is on 7 day calendar and not rolling. This means if a contact gets eligible on Monday they are eligible till Sunday EOD, and everyday the campaign allocation runs and checks whether they are still eligible. If yes the eligibility is still till Sunday EOD if not they are kicked out of the eligibility for that audience.
Slots that were selected or assigned the first time you were made eligible is referred to as "Schedules" we discuss this in a different document/section later.
- not rolling its calendar but eligibility is validated every day - so you can become eligible
- schedules are not recalculated -
- this is created when you first become eligible
- if you become ineligible your schedules are dropped / reset
- min frequency is a parameter schedules take into consideration so if that changes, we need the schedules to drop else it will only get picked in next calendar 7 day period (relative to the person)
- assignment_date is the date when the schedules were distributed for each and every contact
Trigger eligibility nuances
The evaluation of an eligibility of an event and time window can bring about some nuance that are best learned using an example. Lets take the following abandoned cart example:
- Entry/eligibility condition: The eligibility flow runs every day, sees if the add to cart is greater than or equal to 1 and will qualify the contact (make them eligible) for next 7 days
- Exit condition: If the user makes even one purchase or has any purchase_completed event we remove them from eligible contact list
- exit conditions are always on days_1_ (basically looks at last day event)
Lets dry run:
- Day 0: You as a contact become eligible - (
days_30_event_add_to_cart > 0 is True) you are eligible till day 7 - Day 1: You made a purchase, so purchase_completed++ ; eligibility run still keeps you eligible till day 7
- Day 2: Eligibility run sees you did an exit event, throws you out of the eligibility list
- Day 3: You are eligible again even though you did not add any new items to the cart because our day30 count is not 0 (which is correct) and we don't take into account sequence or order of exit events to eliminate what to consider. We evaluate very statically and hence you will become eligible again.
Because:
- we look at exit event only on previous day window and
- also don't consider event ordering to eliminate some add to cart events made before the exit event
- and also don't allow time window on exit events
we end up having unexpected eligibility. This may lead to you being eligible always for instance..
Thats why now we recommend to put the !exit condition in the eligibility condition itself so that when the eligibility runs on day 3 you are not eligible again, because you have made a purchase in last 30 days. (this is just an example the exit condition can be tweaked to 7 days etc)
How do these audiences and triggers eligibility flow into Learning pipeline - What does LP understand?
Campaign allocation -> audience eligibility -> trigger eligibility -> campaign allocation - contact_id to trigger_id
If the condition in the audience definition uses an OR operator -- such as when a contact's country is either US or Singapore -- the system will create two separate campaign_ids: one for US and one for Singapore.
For example, with a simple audience where country = SG, we'll have a specific campaign_id and one row in campaign allocation. The allocation day and expiry day will have a 7-day difference between them.
When we implemented triggers, we wanted to target people in Singapore who made a purchase in the last 7 days. This requires a trigger_id.
The trigger_id visible in the composer is a hash based on the events that define the trigger -- essentially a UUID.
We call this a "compound trigger_id." For historical reasons, the composer didn't originally have the trigger_id, so the Learning Pipeline (LP) created its own ID.
The process works as follows:
- First, we get the list of contacts where country = SG.
- Then, we filter for the subset of contacts who made a purchase in the last 7 days.
- We access the attributes_combined table and remove contacts that don't match the condition of purchases made > 0.
We also maintain trigger_allocations, a temporary table that determines which contacts are eligible for which triggers.
So we have:
- Audience eligibility in a temp table
- Trigger allocation in a temp table
- We perform an inner join (intersection) between the two - refer to
map_trigger_allocations- have not understood this yet - We insert the result or receive this as output of campaign allocations
campaign_id is a representation of the audience filters (the condition we see that were used to build audience in composer)
Schedules
- schedules are not recalculated -
- this is created when you first become eligible
- if you become ineligible your schedules are dropped / reset
- min frequency is a parameter schedules take into consideration so if that changes, we need the schedules to drop else it will only get picked in next calendar 7 day period (relative to the person)
- assignment_date is the date when the schedules were distributed for each and every contact
global timing assignment:
- slots per user per week
- timing slots
- Which slots the user receives a message in for next 7 days -> timing assignment
- Maps the slots to trigger_id --> does this run per week or per day? TBC
- subpart
- assigned slots one week ahead (runs once per week and is not idempotent)
- takes the slot for tomorrow and push it to message_assignment (runs daily and is idempotent) -> this is where slot to trigger_id mapping happens
-> allocation_date and timing_assignment_date should be the same in most cases.
Note the relative calendar 7 days. New user joins on Monday or they become eligible on Monday, your seven day starts then.
Timing runs for subset of users that have allocation.