Skip to main content

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):

  1. A JavaScript event, bryn:personalize: the most flexible option (Recipes 1 to 4).
  2. A global, window.bryn: read the latest at any time (Recipe 6).
  3. 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:

  • individual is null when 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.
  • entityId and individualId are 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.

Show the sales nudge only to companies with pricing intent.Show the sales nudge only to companies with pricing intent.
<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.

Follow up on the page when a demo was started but not finished.Follow up on the page when a demo was started but not finished.

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 individual for null. 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 individualId for connecting to your own analytics (Recipe 7).