The Strategy
Most multi-step lifecycle journeys that need to serve varied content face the same structural problem. To avoid repeating creative across touchpoints, the conventional approach reaches for segmentation: one audience condition per content variant, wired into Audience Paths or Decision Splits. The Canvas branches, each branch carries its own message step, and the QA surface multiplies with every variant added.
This solution removes that pattern entirely using the Canvas Context component, a single Canvas can track which content options have already been shown to a given user, within the journey, in memory, without writing anything to their profile, and randomising dynamically from whatever remains at each step.
The content source can be a Liquid array defined directly in the Context block, or a Braze Catalog. The randomisation is per-user. The Canvas has no branches, no Audience Paths, no User Update steps, and no webhooks. The Context component is the only mechanism required.
How the Canvas is Built
The Canvas structure is linear. Every user follows the same path. What changes between users is not their route through the Canvas, but the content they see at each step.
A Context step fires immediately before every message step. It does two things in a single operation: selects the next unseen content option from the available pool, and records the selection so the pool is smaller next time. The message step that follows simply renders whatever the Context block just selected.
There are no User Update steps. There are no webhooks. Nothing writes back to the user profile.
What the Context Block Does at Each Step
The Context component maintains a single shown variable, which holds a comma-separated string of the content identifiers already served to this user in this journey.
- At step 1,
shown is empty. The full pool is available - At step 2,
shown contains one identifier. Three remain - At step 3, two remain
- At step 4, exactly one remains
The Liquid logic at each Context block:
{% comment %} Define the content pool — Liquid array or Catalog key list {% endcomment %}
{% assign all = "editorial,recommendation,reward,offer" | split: "," %}
{%
comment %} Read what has been shown so far in this journey {% endcomment %}
{% assign shown_array = context.${shown} | default: "" | split: "," %}
{% comment %} Build the remaining pool by excluding already-shown options {% endcomment %}
{% assign available = "" %}
{% for v in all %}
{% assign already_shown = false %}
{% for s in shown_array %}
{% if s == v %}
{% assign already_shown = true %}
{% endif %}
{% endfor %}
{% unless already_shown %}
{% assign available = available | append: "," | append: v %}
{% endunless %}
{% endfor %}
{% comment %} Randomise from remaining pool using epoch seconds as seed {% endcomment %}
{% assign pool = available | split: "," %}
{% assign seed = 'now' | date: '%s' | modulo: pool.size %}
{% assign selected = pool[seed] %}
{% comment %} Update shown — this is what the next Context block will read {% endcomment %}
{% assign updated_shown = context.${shown} | default: "" | append: "," | append: selected %}
The message step that follows renders {{context.${selected}}}, or uses selected as a lookup key into a Braze Catalog.
- Implementation note on the seed: Epoch seconds (
%s) are used rather than nanoseconds (%N). When Canvas steps execute in close succession, as they can across users entering simultaneously, they can produce the same index for different users. - Implementation note on the array: Liquid has no native array type. The pool lives as a comma-separated string and is materialised into an array using
split only when iteration or indexing is needed. append builds the string back up. This is the idiomatic pattern throughout.
Per-User Randomisation
Two users entering the same Canvas at the same time will receive different content sequences. The seed is derived from the moment each user's Context step executes, with independent timing, independent index, independent selection.
User A at step 1 might have a seed that maps to index 2 (variant C). User B at step 1 might have a seed that maps to index 0 (variant B). Neither is aware of the other. Neither needs a segment to differentiate them. The randomisation is an emergent property of when their steps run.
Across four steps with four variants, both users see all four options exactly once. The order differs. Nothing else does.
Content Source (Array vs Catalog)
The variant pool is defined at the top of the first Context block's Liquid. It can be:
- A Liquid array: identifiers listed inline, resolved against Content Blocks or Liquid conditionals in the message step. Suitable for smaller, stable creative sets. Content updates require a Canvas or Content Block edit.
- A Braze Catalog: identifiers in the Context block are catalog row ids from a Catalog. The message step uses columns from the catalog row to resolve the content. Creative updates happen in the catalog. The Canvas is never touched.
The catalog pattern is preferable in production because it decouples the content from the Canvas architecture. Seasonal refreshes, A/B copy tests, and offer updates can all happen in the catalog without requiring a Canvas edit or re-QA.
What This Removes From the Build
No Audience Paths or Decision Splits to route by variant. No segments to build or maintain. No User Update steps to write variant state to the profile. No webhooks to trigger external tracking. No cleanup Canvases to reset stale attribute values.
The shown variable lives inside the Canvas session and expires when the journey ends. It is not a profile attribute. It is not queryable. It is not visible anywhere outside this Canvas run. That is the correct scope for a piece of state that only matters for the duration of one user's journey.
The Canvas has one path. Every user takes it. What varies is what they see at each message step, which is determined at runtime, per user, in the Context block immediately before each step fires.
The Shift in How to Think About It
The conventional approach asks: which users should see which variant? That question leads to segments, conditions, and branching.
The Context approach asks: what has this user already seen in this journey, and what should they see next? That question leads to a memory variable, a shrinking pool and a single linear Canvas.
The Canvas does not need to be told which users belong to which variant group. It needs to remember, for each user individually, what it has already given them, and then select from what remains. Canvas Context is exactly the right tool for that problem.
Greg Lakatos
Solutions Architect @ APPLY