Instrumenting Audiences
The Apex audience builder lets you define who a campaign or journey targets via predicates over events and user traits. For an audience to match anyone, your codebase has to be firing those events and setting those traits. This guide tells you what to wire up.
The chain
Your codebase → apex.track(...) / apex.identify(...) → Apex stores the event + traits → Audience predicate matches → Campaign / journey fires
Every audience predicate points at one of three things:
- An event — fired via
apex.track("event_name", { ...properties }). Predicates:event_fired,event_not_fired. - A trait — set via
apex.identify(userId, { trait_name: value }). Predicate:attribute. - A channel preference — set by your end-users via the public preferences page. Predicate:
channel_opted_in/channel_opted_out. No codebase work required for this third type.
Starter audiences shipped with your workspace
When you first visit Marketing Campaigns, Apex installs five reusable audience seeds. Each requires specific instrumentation. Here's exactly what to fire.
Active customers (90d)
Predicate: purchase_completed fired in the last 90 days.
Add to your codebase — call this from your checkout-success handler:
import { track } from "@apex-inc/sdk";
track("purchase_completed", {
orderId: order.id,
total: order.total, // e.g. 149.99
currency: order.currency, // e.g. "USD"
});
If you're using the script tag (Shopify, WordPress, Webflow), the same call is apex.track("purchase_completed", { ... }).
Inactive 30+ days
Predicate: session_started NOT fired in the last 30 days.
Add to your codebase — call this once per user session, typically on app load:
import { track } from "@apex-inc/sdk";
track("session_started", {
path: window.location.pathname, // optional
});
You don't need to fire this for the audience to MATCH (the predicate is event_not_fired). But you DO need it firing for active users so Apex knows who's currently engaged. Without it, every user looks inactive.
Trial users
Predicate: plan trait equals "trial".
Add to your codebase — call this after login, and again whenever the user's plan changes:
import { identify } from "@apex-inc/sdk";
identify(user.id, {
email: user.email,
plan: user.plan, // "trial" | "starter" | "pro" | ...
});
Engaged, not yet purchased
Predicate: session_started in last 30 days AND no purchase_completed ever.
Wire BOTH events from above. This audience composes the two — wire each event the way the prior two sections describe.
All marketing-opted-in
Predicate: email channel preference is opted-in (or default).
No codebase work required. End-users set their channel preferences via the public preferences page at /preferences/[token]. The default state is opted-in until they explicitly opt out — this audience matches everyone except explicit opt-outs.
Per-framework code
The SDK has the same surface across frameworks. Pick yours:
React / Next.js
import { track, identify } from "@apex-inc/sdk";
// In an event handler:
function handleCheckoutSuccess(order) {
track("purchase_completed", {
orderId: order.id,
total: order.total,
currency: order.currency,
});
}
// At login (inside an effect or auth callback):
async function onLogin(user) {
identify(user.id, { email: user.email, plan: user.plan });
}
Vue / Nuxt
import { track, identify } from "@apex-inc/sdk";
// Inside a composable or methods:
function checkoutSuccess(order) {
track("purchase_completed", {
orderId: order.id,
total: order.total,
currency: order.currency,
});
}
Vanilla / Script tag (Shopify, WordPress)
After the snippet loads:
<script>
apex.track("purchase_completed", {
orderId: "{{ order.id }}",
total: {{ order.total }},
currency: "{{ order.currency }}",
});
</script>
For Shopify specifically, drop this in your "Order Status" page (Settings → Checkout → Additional scripts).
Python (server-side)
from apex import track, identify
track("purchase_completed", {
"orderId": order.id,
"total": order.total,
"currency": order.currency,
})
Ruby (server-side)
require 'apex'
Apex.track("purchase_completed", {
order_id: order.id,
total: order.total,
currency: order.currency,
})
Verifying your events are landing
Three ways:
-
Audience builder side panel. Open
/dashboard/audiences, edit any audience that uses an event. The "Wire your code" panel shows the snippet, and the EventNamePicker autocompletes events Apex has actually received from your workspace. Events with non-zero counts have arrived. -
The campaigns wizard's "Send a test event" button. When you click "Use template" on a campaign, the wizard's first screen has a button that fires a test event with
is_test=1. Clicking it confirms the pipe end-to-end. After that, fire from your real code; the wizard auto-advances when it sees a non-test arrival. -
The MCP server's
wire_audience_seedtool. If you're using Cursor or Claude Code with the Apex MCP, ask your AI agent to wire instrumentation directly: "Use the Apex MCP'swire_audience_seedtool with seedId='aud-seed-active-customers-90d'." The agent reads the seed's instrumentation requirements, generates the code for your framework, edits the right file, and fires a test event to verify the wiring landed.
Authoring your own audiences
The seeds are starting points. Once your codebase is firing events that fit your business (subscription_renewed, feature_xyz_used, support_ticket_opened), build audiences from those:
- Open
/dashboard/audiences. - New audience → Match by rules → Add an
event_firedpredicate → start typing your event name. The picker auto-completes events Apex has seen from your workspace plus events declared inSTARTER_TRIGGER_CONTRACTS. - The "Wire your code" side panel updates as your predicate changes — copy the snippet for your framework.
Related
- Tracking Events — the full
track()API reference. - Identity — the
identify()API and trait stitching. - Journey Audiences — the predicate language overview.
- Channels — channel preferences and opt-in audiences.