pex

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:

  1. An event — fired via apex.track("event_name", { ...properties }). Predicates: event_fired, event_not_fired.
  2. A trait — set via apex.identify(userId, { trait_name: value }). Predicate: attribute.
  3. 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:

  1. 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.

  2. 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.

  3. The MCP server's wire_audience_seed tool. If you're using Cursor or Claude Code with the Apex MCP, ask your AI agent to wire instrumentation directly: "Use the Apex MCP's wire_audience_seed tool 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:

  1. Open /dashboard/audiences.
  2. New audience → Match by rules → Add an event_fired predicate → start typing your event name. The picker auto-completes events Apex has seen from your workspace plus events declared in STARTER_TRIGGER_CONTRACTS.
  3. The "Wire your code" side panel updates as your predicate changes — copy the snippet for your framework.