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_namestyle); see the RFC 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 the grammar The shape isdomain.event_name — for example:
order.shippeduser.signed_upinvoice.paidcart.abandoned_after_30minsubscription.renewed
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_upnotsign_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.shippedis easier to scan and group thanshipped_order.
Properties
Properties are an arbitrary JSON object you attach to an event:- Recipient resolution — the orchestrator extracts
userId,email, or whatever your campaign keys on, directly or via an enricher. - Template substitution —
{{ trackingUrl }}in the notification content resolves to the property value at delivery time. - 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) andBigInt. - Don’t repeat the orchestrator’s job. If you can compute the recipient list from a
userId, just passuserIdand 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 theMissing 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.- The orchestrator gets a schema, not just observed shapes. When a campaign listens on this event, the AI orchestrator receives
description,schema, andidempotencyFieldsdirectly in its context — no more inventing properties that don’t exist. - The dashboard catalog reflects what your code says. Description, declared schema, and last-seen timestamps appear in the dashboard, browsable by environment.
- 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 backendGETs 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.
| Case | soft (default) | strict |
|---|---|---|
| Event not declared | console.warn, event still sent | console.error, SDK does not push |
| Payload doesn’t match the declared schema | console.warn, event sent byte-for-byte unchanged | console.error, SDK does not push |
| All good | silent | silent |
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:{ duplicate: true, idempotencyKey } (status 200) and does not re-enqueue. See Event Tracking for the wire format and idempotency plan for the design.
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:
| Analytics events (Segment, Mixpanel, …) | Notifizz events | |
|---|---|---|
| Purpose | Record what users did | Trigger notifications |
| Latency budget | Bulk-loaded later | Real-time |
| Volume | Every page view, every click | One per real-world product action |
| Cardinality | Hundreds of unique names | Tens, sometimes a hundred |
| Properties | Anything that helps analysis | Just enough for the campaign |
FAQ
snake_case / kebab-case / dot.notation — which?
snake_case / kebab-case / dot.notation — which?
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.Can I rename an event after a campaign uses it?
Can I rename an event after a campaign uses it?
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.
I added a new property — does the old campaign still work?
I added a new property — does the old campaign still work?
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.
How do I model 'cart abandoned after 30 minutes'?
How do I model 'cart abandoned after 30 minutes'?
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.Can the same event drive multiple campaigns?
Can the same event drive multiple campaigns?
Yes — fan-out is automatic. Every campaign listening on the event creates its own workflow instance independently. See campaigns for what happens after match.
What about event categories or transactional vs marketing?
What about event categories or transactional vs marketing?
The event itself is plain — no category. The campaign picks a category (transactional or promotional) which decides queue priority. Same event can drive both a transactional campaign and a promotional one.
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().