Skip to main content

Events

An event is the unit of input your application sends to Notifizz. Naming, properties, and idempotency are decisions that shape every campaign that listens on the event — get them right early and the rest of the pipeline stays simple.

TL;DR

  • Names follow a stable grammar (domain.event_name style) — see Naming below for the exact rules.
  • Properties are arbitrary JSON the orchestrator can read; they double as recipient identifiers and template values.
  • Idempotency keys deduplicate retried emits — set a deterministic key for any retried job.
  • The dashboard surfaces “missing properties” when a campaign expects something the event didn’t send.

Naming

Event names follow a domain.event_name grammar — for example:
  • order.shipped
  • user.signed_up
  • invoice.paid
  • cart.abandoned_after_30min
  • subscription.renewed
Names go through validateEventName server-side. Invalid names are rejected with 400 event/invalid-name and a reason field in the response body.

Conventions

  • Lowercase + underscores or dots. Don’t mix camelCase. order.shipped ✅; orderShipped ❌.
  • Past tense for verbs. Events describe things that happened. signed_up not sign_up.
  • Stable names. Renaming an event after a campaign keys on it breaks the campaign — update the campaign in the dashboard before/together with the rename.
  • Domain-first. order.shipped is easier to scan and group than shipped_order.

Properties

Properties are an arbitrary JSON object you attach to an event:
Three things consume properties:
  1. Recipient resolution — 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 value at delivery time.
  3. Routing context — campaigns can branch on properties (e.g. if (properties.plan === "pro")).

Rules of thumb

  • Pass enough context for the campaign, not your whole DB row. Properties are persisted on the workflow instance for traceability — keep them lean.
  • Stable shape beats stable values. Adding a new property is safe; renaming an existing one breaks campaigns that reference the old name.
  • Strings, numbers, booleans, nested objects, arrays. All JSON-serialisable types work. Avoid Date (use ISO strings) and BigInt.
  • Don’t repeat the orchestrator’s job. If you can compute the recipient list from a userId, just pass userId and let an enricher fetch the rest at delivery time.

Missing properties

When a campaign reads a property the event didn’t send, the orchestrator either branches gracefully or surfaces it as a “missing property” in the dashboard. Use the Missing properties view per environment to find events where you’ve under-shared with the campaign — usually a sign that you renamed something on one side without the other.

Declaring events (catalog)

Tracking an event is enough to make Notifizz process it — declared or not. Declaring goes one step further: it writes the event into a catalog the orchestrator and the dashboard can read, and gives the AI a precise contract for the event’s properties instead of having to guess from observed payloads.
Three things change once an event is declared:
  1. The orchestrator gets a schema, not just observed shapes. When a campaign listens on this event, the AI orchestrator receives description, schema, and idempotencyFields directly in its context — no more inventing properties that don’t exist.
  2. The dashboard catalog reflects what your code says. Description, declared schema, and last-seen timestamps appear in the dashboard, browsable by environment.
  3. Schema-based validation in dev, opt-in via mode (see below).

How it propagates

Declaring is a local act on your client; the server picks it up through the discovery webhook that already serves enrichers. On a 30-minute cycle (or when you click “Refresh” in the dashboard), the backend GETs your handler URL and reads { enrichers, events } from the response. New events appear in the catalog; removed events shift to removed status with their last-known schema preserved.

Validation modes — soft (default) and strict

schemaMode is purely client-side — it controls how the SDK behaves when an event you track() doesn’t match its declared schema. The server never blocks an event based on a declared schema; it only records mismatches as audits in the dashboard. strict is a local guard rail for catching bugs in dev/CI. The server stays permissive — events from Segment, webhook bridges, or non-SDK code paths always go through, regardless of any declared schema.
Declaring an event is optional. Tracking continues to work without declareEvent — the event simply appears as an “orphan” in the dashboard with its observed properties as the only source of truth for the orchestrator. Declaring is the path to a typed catalog and a stricter local feedback loop.

Idempotency

Every event carries an idempotency key. The SDK auto-generates a UUID when you don’t provide one — that’s fine for fire-once emits but defeats deduplication on retries. For any retried emit (queued job, scheduler, retry middleware, webhook handler), set a deterministic key derived from your domain:
A retry with the same key returns { duplicate: true, idempotencyKey } (status 200) and does not re-enqueue. See Event Tracking for the wire format and idempotency plan for the design.

Event time

An event has two timestamps, and they are not the same thing: when Notifizz received it, and when the business fact happened. In the common case — you track at the moment the thing occurs — reception time is the truth and there is nothing to declare. They come apart as soon as emission is deferred: a queue replayed after an incident, a batch collected offline, a history backfilled at migration time. Declaring occurredAt keeps the event honest about the fact it describes:
This is not cosmetic. The declared time feeds time-scoped resolutions server-side — answering “who were the subscribers as of that moment” rather than “who are they now”. A backfill that claimed reception time would resolve its audience against today’s state and quietly send to the wrong people. For the same reason a future date is refused (event/invalid-occurred-at, with a 60-second tolerance for clock skew): an event may not assert a state that does not exist yet. Available on all three backend SDKs — see Node, Java and PHP.

Mental model: events ≠ Segment events

If you come from a Segment-like analytics tool, the model looks similar (track calls, properties), but the meaning differs: Use both side by side — Segment for analysis, Notifizz for triggering campaigns. Or use the Segment connector to bridge both worlds and avoid double-instrumentation.

FAQ

The grammar is permissive; the convention is domain.event_name. Use lowercase dots between domains and underscores within a name component (order.shipped, user.signed_up). Avoid mixing kebab-case and camelCase — pick one and stick to it across your team.
Yes, but the rename is a two-step migration: (1) update the campaign in the dashboard to listen on the new name; (2) update your code to emit the new name. Do them close together — between the two, the campaign won’t run on the old name.
Yes. Properties are extensible — adding a new field is safe; existing campaigns ignore what they don’t read. The campaign only breaks if it needs a property that’s no longer being sent.
Two patterns: (1) emit cart.abandoned from your backend after a server-side timer; (2) emit cart.created and let the campaign’s orchestrator schedule the delayed step. (1) keeps timing logic in your domain; (2) keeps everything in Notifizz. Both are common.
Yes — fan-out is automatic. Every campaign listening on the event creates its own workflow instance independently. See campaigns for what happens after match.
The event itself is plain — no category. The campaign picks one of three categories — transactional, product or marketing — which decides queue priority and whether the message carries an unsubscribe link. The same event can drive a transactional campaign and a product one side by side. See campaigns.

See also

Campaigns

Lifecycle, statuses, versioning — what runs after the event matches.

Event Tracking

HTTP wire format, idempotency contract.

Recipients

How event properties become recipient lists.

Connector webhooks

Segment / Stripe / HubSpot / Intercom — alternatives to direct track().