Skip to main content

Notification event design

Events and campaigns are the two surfaces a Notifizz user touches. Together they define what happened, what to do about it, and who it concerns — but each lives in a different place.

TL;DR

  • An event is what your code emits with client.track(eventName, properties). No campaign id, no recipient list.
  • A campaign is a dashboard-defined reaction to an event — its orchestrator code, channels, and content.
  • Properties ride on the event; the campaign’s orchestrator (and any registered enricher) turn them into recipients and template values.
  • The same event can drive multiple campaigns without code changes.

Events

An event is a single fact about your application. You name it, you attach properties, you emit it.
await client.track("order.shipped", {
  orderId: "ORD-4521",
  userId: "u_42",
  carrier: "FedEx",
  trackingUrl: "https://tracking.example.com/4521",
});
The shape is intentionally flat — (eventName, properties, options). The SDK never references workflows, campaigns, or recipients.

Naming events

Events follow a domain.event_name shape — for example order.shipped, user.signed_up, invoice.paid. The grammar is Stable names matter: campaigns key on them, and renaming an event after a campaign ships breaks the wire-up.

Properties

Properties are arbitrary JSON-serialisable data — strings, numbers, booleans, nested objects. They serve three jobs:
  1. Recipient identification — the orchestrator extracts userId, email, or whatever your campaign keys on, directly or via an enricher.
  2. Template substitution{{ trackingUrl }} in the notification content resolves to the property.
  3. Routing context — campaigns can branch on properties (e.g. send a different notification when plan === "pro").
Pass enough context for the campaign to do its job, but don’t dump your whole DB row — properties are persisted on the workflow instance for traceability.

Idempotency

Every event carries an idempotency key. Auto-generated UUIDs are fine for one-off emits; for retried jobs, set a deterministic key derived from your domain:
await client.track(
  "order.shipped",
  { orderId, userId },
  { idempotencyKey: `order-shipped:${orderId}` },
);
A retried emit with the same key short-circuits at the backend ({ duplicate: true }). See Event Tracking reference.

Campaigns

A campaign (the term workflow is used interchangeably in older docs and in some backend code paths) is a dashboard-configured reaction to an event. It owns:
  • The event name it listens on.
  • An orchestrator — generated code that builds the recipient list from event properties + enrichers.
  • One or more channels with their templates (Notification Center config, email config, …).
  • A status (Editing / Dev / Live / Offline / Archived) that controls whether it runs in which environment.
Crucially, the SDK call site never references the campaign. You don’t pass an id, you don’t list recipients. When order.shipped fires, every campaign listening on order.shipped runs its own orchestrator independently — fan-out is automatic.

One event, many campaigns

This is the common pattern: a single product event drives several distinct notifications. Each campaign decides its own recipients (from event properties or an enricher), its own channel, its own template. Adding a new campaign is a dashboard task — no code change.

Versioning and lifecycle

Campaigns have five statuses (Editing, Dev, Live, Offline, Archived) and the runtime gate is per-environment — Dev runs on dev only, Archived is terminal. The full status table, runnable filter, and version semantics are in campaigns concept (ADR-grounded). Each save creates a new version — in-flight workflow instances run against the version they started on, even if a newer version has shipped since.

Recipients

Recipients are produced by the campaign’s orchestrator, not by the SDK call site. The orchestrator runs server-side, reads the event properties, and returns a list of recipient objects. Each must have at minimum:
{ id: "u_42", email: "alice@example.com" }
Additional fields can be attached for use in templates (displayName, locale, timezone, …). The orchestrator can:
  • Extract recipients directly from event properties{ userId, email } baked into the event.
  • Resolve them via an enricher — a server-side function the orchestrator calls to fetch user data live (enrichers tutorial).
  • Pull from a dynamic source — saved audiences, segments (dynamic recipient sources plan).
Three patterns; pick the one that matches the freshness and coverage you need.

Where each piece lives

ConceptWhere it’s definedSurface
EventYour backend codeclient.track(eventName, properties)
CampaignNotifizz dashboardOrchestrator + channels + status
PropertiesYour backend codePassed to track()
RecipientsCampaign’s orchestrator (server-side)Output of orchestrator code
That column-three / column-two split is the core invariant: the SDK is data-only, the dashboard owns intent and routing.

FAQ

No — that’s the deliberate constraint. Put userId in the event properties and let the campaign’s orchestrator return that user as the only recipient. If you find yourself wanting “send this exact notif to this exact user”, model it as a campaign listening on a dedicated event.
They keep running against the version they started on. New events trigger the new version. Versioning prevents mid-flight surprises.
No — the campaign keys on the event name. Either revert the rename in your code, or update the campaign in the dashboard to listen on the new name. There is no auto-redirect.
No. Each campaign creates its own workflow instance, runs its own orchestrator, and dispatches independently. They share nothing beyond the inbound event payload.
Two options: (1) emit cart.abandoned from your backend after a server-side timer; or (2) emit cart.created and let the campaign’s orchestrator schedule the delayed step. Option (1) keeps timing logic in your domain; option (2) keeps everything in Notifizz. Both are common.
Same primitive. A transactional event (invoice paid, order shipped) and a marketing event (we miss you) both flow through track(). Priority is decided per campaign — transactional campaigns jump the queue.

See also

How Notifizz works

The event-driven pipeline, end to end.

Channels

Notification Center widget, email, what’s next.

Event Tracking reference

HTTP wire format, idempotency contract.

Backend quickstart

Send your first event in under five minutes.