Website Personalization Recipes
Copy-paste snippets for the team that manages a Bryn customer's website (often
non-engineers). Each snippet is paste-ready: drop it in a <script> alongside the Bryn
pixel snippet, or hand it to a developer.
Before you start: the pixel must be live on your site. See Install the pixel.
How it works: Bryn announces, your code reacts
When Bryn recognizes a visitor, it announces it on the page; your code listens and reacts. Bryn never changes your page for you: you decide what to show.
It announces in three ways (use whichever suits you):
- A JavaScript event,
bryn:personalize: the most flexible option (Recipes 1 to 4). - A global,
window.bryn: read the latest at any time (Recipe 6). data-bryn-*attributes: no JavaScript needed (Recipe 5).
What you get back
{
status: "resolved", // "pending" while Bryn is still working; "resolved" when it knows the visitor
entity: { // the COMPANY (always present once resolved)
entityId: "39dbb553-...", // stable id for this company: echo it back on server-side events
companyName: "Acme Corp",
domain: "acme.com",
industry: "...", country: "...", employees: "...", stage: "...",
signals: ["pricing_page_viewed", "high_intent"] // what the company has done
},
individual: { // the PERSON, or null if Bryn only knows the company
individualId: "5f2b8c1e-...", // stable id for this person: echo it back on server-side events (see Recipe 7)
firstName: "Alex",
title: "VP Marketing",
email: "...", lastName: "...", seniority: "...", linkedin: "...",
signals: ["demo_requested"] // what this person has done
}
}
Three rules:
individualisnullwhen Bryn knows the company but not the specific person. Always check for it.- The score is never sent to your page. You react to who they are (
entity/individual) and what they've done (signals), not to a number. entityIdandindividualIdare stable identity keys. Capture them and send them back on your server-side events so both sources stitch to the same records: see Server-side events & signing.
individualId is a stable identifier for the resolved person. Hand it to your own analytics
tool to connect this visit to your records (see Recipe 7). It's never shown to the visitor.
Recipe 1: Greet a returning person by name
Best when you want a personal touch and only when Bryn actually knows the person.
<script>
window.addEventListener("bryn:personalize", (event) => {
const { status, individual } = event.detail;
if (status !== "resolved" || !individual) return; // not resolved, or company-only
// Example: show a banner element you already have on the page
const banner = document.querySelector("#welcome-banner");
banner.textContent = `Welcome back, ${individual.firstName}!`;
banner.style.display = "block";
});
</script>
Recipe 2: Account-level greeting (company, no name)
Fires far more often than Recipe 1 (Bryn resolves the company for most visits). Keep the tone light: you inferred the company, the visitor didn't tell you who they are.
<script>
window.addEventListener("bryn:personalize", (event) => {
const { status, entity, individual } = event.detail;
if (status !== "resolved" || individual) return; // handle the company-ONLY case here
const banner = document.querySelector("#welcome-banner");
banner.textContent = `Exploring Bryn for the team at ${entity.companyName}? We can help.`;
banner.style.display = "block";
});
</script>
Recipe 3: Person if known, otherwise company (the common combined pattern)
<script>
window.addEventListener("bryn:personalize", (event) => {
const { status, entity, individual } = event.detail;
if (status !== "resolved") return;
const banner = document.querySelector("#welcome-banner");
banner.textContent = individual
? `Welcome back, ${individual.firstName}!` // we know the person
: `Someone from ${entity.companyName} is here. Say hello.`; // company only
banner.style.display = "block";
});
</script>
Recipe 4: Only react when there's buying intent
Being recognized isn't the same as being interested. Gate on a signal so you only act
on visitors doing something meaningful. Your active signals are the ones you
configured in Bryn (e.g. pricing_page_viewed, demo_requested):
use your own names here.
<script>
window.addEventListener("bryn:personalize", (event) => {
const { status, entity } = event.detail;
if (status !== "resolved") return;
// Show a "Talk to sales" nudge only if the company has shown pricing intent
if (entity.signals.includes("pricing_page_viewed")) {
document.querySelector("#talk-to-sales").style.display = "block";
}
});
</script>
Recipe 5: No JavaScript at all, the data-bryn-* attributes
Personalize purely in your HTML, with no script at all. Often the best fit for non-engineers.
Every attribute takes a bryn path into the payload (entity.companyName,
individual.firstName, signals, status, …; a bare companyName checks both sections,
person first).
Fill in text. data-bryn-field sets an element's text; your fallback stays if the
field isn't known for this visitor:
<h1>Welcome back, <span data-bryn-field="entity.companyName">your team</span>!</h1>
<p>Nice to see you, <span data-bryn-field="individual.firstName"></span>.</p>
Show / hide. data-bryn-show / data-bryn-hide reveal or hide an element on a
condition (a path that's present/true, or path=value). Author the default state in
your markup; Bryn toggles the standard hidden attribute once it resolves, so the page is
never broken while cold:
<!-- greet the person only when Bryn knows who they are -->
<div hidden data-bryn-show="individual.firstName">
Welcome back, <span data-bryn-field="individual.firstName"></span>!
</div>
<!-- show a pricing CTA only when the company viewed pricing (use your own signal names) -->
<div hidden data-bryn-show="signals.pricing_page_viewed">Talk to sales about pricing →</div>
<!-- a loading line that disappears once resolved -->
<div data-bryn-hide="status=resolved">Personalizing…</div>
Recipe 6: Read the latest at any time (window.bryn)
If your code runs after Bryn has already resolved (e.g. on a button click), read the global instead of waiting for the event:
<script>
document.querySelector("#book-demo").addEventListener("click", () => {
const visitor = window.bryn; // latest payload, or undefined if not resolved yet
if (visitor?.status === "resolved") {
// e.g. prefill a form or tag your analytics
analytics.track("demo_click", { company: visitor.entity.companyName });
}
});
</script>
Recipe 7: Pass the recognized person into your analytics
Bryn gives each resolved person a stable identifier, individual.individualId. Hand it to
your own analytics tool to tie this visit to your records, for example PostHog's
posthog.identify(). Bryn doesn't call your analytics for you; you make the call, alongside
your existing analytics snippet:
<script>
window.addEventListener("bryn:personalize", (event) => {
const { status, individual } = event.detail;
if (status !== "resolved" || !individual) return; // only when Bryn knows the person
// Hand the Bryn identifier and the full payload to your analytics tool, e.g. PostHog:
posthog.identify(individual.individualId, { bryn: event.detail });
});
</script>
Only act when there's a person: when individual is null, Bryn knows the company but not
who the visitor is, so there's nothing to identify.
Recipe 8: Follow up on an abandoned demo
A worked example of one signal, end to end. When someone starts your product demo and leaves before finishing, you can offer to pick up where they left off on their next visit.
First, tell Bryn about the abandon. Your analytics sends a "demo abandoned" event (for
example demo_abandoned) to PostHog; deciding what counts as abandoned is up to your
product. In Bryn, go to Configuration, then Signal Mapping, and
map it: when demo_abandoned happens, raise demo_abandonment.
That event now becomes a demo_abandonment signal on the visitor, and it rides the pixel
like any other signal.
Then react to it on your site. Gate a call-out on the signal, just like Recipe 4:
<script>
window.addEventListener("bryn:personalize", (event) => {
const { status, entity, individual } = event.detail;
if (status !== "resolved") return;
if (!entity.signals.includes("demo_abandonment")) return; // only unfinished demos
const banner = document.querySelector("#pick-up-banner");
banner.textContent = individual
? `Welcome back, ${individual.firstName}. Want to finish the demo?`
: `Looks like your team started the demo. Want a hand finishing it?`;
banner.style.display = "block";
});
</script>
Prefer no JavaScript? The data-bryn-* attributes work too: data-bryn-show accepts a
signals.<type> membership check:
<!-- shown only for visitors with an active demo_abandonment signal -->
<div hidden data-bryn-show="signals.demo_abandonment">
Pick up where you left off in the demo →
</div>
Bryn can also stage a reviewed follow-up for these visitors: the "Follow up when a demo is left unfinished" Play on your Plays screen. Nothing sends until you turn it on and approve.
Good habits
- Always keep a sensible default. A cold or unknown visitor gets
status: "pending"and no data: your page must look right before Bryn does anything. Never hide content behind personalization. - Check
individualfornull. Company-only is the common case; don't assume a name. - Personalize, don't startle. For company-only (inferred from network, not self-identified), keep it helpful and low-key rather than "we know exactly who you are."
- Nothing sensitive reaches the page. No scores, no internal company IDs, just safe
display attributes, the signal labels you configured, and the person's stable
individualIdfor connecting to your own analytics (Recipe 7).