> ## Documentation Index
> Fetch the complete documentation index at: https://notifizz.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Synthetic Events

> Events Notifizz generates for you — on a schedule, grouped over a window, on inactivity, or when a counter crosses a threshold.

# Synthetic events

Most events come from your application: you call `track(...)` and Notifizz reacts. **Synthetic events** are the opposite — Notifizz generates them for you, then they flow through the exact same pipeline as any other event. Your orchestrator consumes them identically; nothing special to learn on the receiving side.

## TL;DR

* Create them from **Events → New event**. No code, no redeploy.
* Four types: **Scheduled**, **Grouped**, **Reminder**, **Milestone**.
* A synthetic event is a normal event: campaigns subscribe to it, your orchestrator reads `event.properties` as usual.
* Every synthetic event carries a `_synthetic` marker in its properties so your orchestrator can tell which configuration produced it.

## The four types

| Type          | Fires when…                                             | Typical use                                      |
| ------------- | ------------------------------------------------------- | ------------------------------------------------ |
| **Scheduled** | a fixed time arrives (daily, weekly, monthly)           | daily recap, weekly newsletter, monthly reminder |
| **Grouped**   | several events accumulate over a window                 | "you have 12 new comments" instead of 12 pings   |
| **Reminder**  | an expected follow-up event does **not** happen in time | abandoned cart, unfinished onboarding            |
| **Milestone** | a counter crosses a threshold                           | 100th order, 5 failed logins                     |

<Note>
  All four types are available. Create any of them from **Events → New event**.
</Note>

## Scheduled

A Scheduled event fires at a fixed time: every day, every week, every month, or on a custom schedule. Pick the time and timezone in the dashboard — no cron syntax required (a custom expression is available for advanced users).

### Timing guarantee

A Scheduled event is emitted **within 5 minutes** of its configured time. This is intentional — notifications aren't time-critical to the second, and the small window keeps delivery reliable. Plan your messaging copy accordingly (say "this morning", not "at exactly 9:00:00").

### What your orchestrator receives

The event carries the scheduled instant and nothing else — no recipient list. Your orchestrator decides who to notify, typically by reading your audience through an [enricher](/docs/concepts/enrichers):

```ts theme={null}
export const orchestrate = async (event: Event, campaign: Campaign, sdk: Sdk): Promise<Recipient[]> => {
  // event.properties._synthetic.kind === 'cron'
  // event.properties.scheduledAt  → the instant it was scheduled for (epoch ms)

  const team = await sdk.enrichWith('activeMembers', {});
  return team.members.map((m) => ({
    id: m.userId,
    title: `Your daily recap, ${m.firstName}`,
  }));
};
```

Because the timing producer carries no audience, the cost stays flat: one event per tick, your orchestrator fans out to recipients itself.

## Grouped

A Grouped event bundles many occurrences of one event into a single notification. Instead of 12 separate "new comment" pings, your user gets one "12 new comments" message. Pick the event to group, who to group it for, and when to send.

### Group for whom

Every summary is scoped **per user** by default. Add more dimensions (e.g. a conversation or document id) to keep separate summaries side by side — "3 comments on doc A" and "5 comments on doc B" stay distinct rather than merging into "8 comments".

### When the summary is sent

Two strategies:

* **At regular intervals** — the summary ships every *N* minutes/hours, regardless of activity. Predictable cadence (hourly recap, daily digest).
* **When activity quiets down** — the summary ships once *N* minutes/hours pass with no new event. Great for conversations: you accumulate while it's active, and send once it settles.

A summary also ships early if it reaches 100 accumulated events, so a busy window never grows unbounded.

### What your orchestrator receives

The grouped event carries the accumulated occurrences:

```ts theme={null}
export const orchestrate = async (event: Event, campaign: Campaign, sdk: Sdk): Promise<Recipient[]> => {
  // event.properties._synthetic.kind === 'digest'
  const items = event.properties.events;       // the accumulated occurrences
  const count = event.properties.count;        // how many
  const userId = event.properties.aggregationKey; // who/what this summary is for

  const user = await sdk.enrichWith('userProfile', { userId });
  return [{ id: user.id, title: `You have ${count} new comments` }];
};
```

## Reminder

A Reminder fires when an expected follow-up **doesn't** happen in time. You pick a trigger event, the events that would cancel it, and a delay. If the delay passes with no cancelling event, the reminder fires.

Classic uses: abandoned cart (`cart.created` with no `checkout.completed` in 1h), unfinished onboarding, or a dormant account.

The trigger and cancelling events keep flowing through your normal campaigns — the reminder is an extra timer on top, not a replacement. The reminder event carries the original trigger's data:

```ts theme={null}
export const orchestrate = async (event: Event, campaign: Campaign, sdk: Sdk): Promise<Recipient[]> => {
  // event.properties._synthetic.kind === 'inactivity'
  const cart = event.properties.triggerEvent; // the payload that armed the timer
  const user = await sdk.enrichWith('userProfile', { userId: cart.userId });
  return [{ id: user.id, title: 'Still thinking it over?' }];
};
```

## Milestone

A Milestone fires when a counter crosses a threshold — the 100th order, the 5th failed login, the first publication. Set one or several thresholds; each fires once.

Choose how the counter resets:

* **All-time** — never resets. "100th order ever."
* **Each month** — resets on the 1st. "100 orders this month."
* **Rolling** — only counts the last *N* minutes/hours. "5 failed logins in the last hour."

```ts theme={null}
export const orchestrate = async (event: Event, campaign: Campaign, sdk: Sdk): Promise<Recipient[]> => {
  // event.properties._synthetic.kind === 'threshold'
  const reached = event.properties.thresholdReached; // e.g. 100
  const userId = event.properties.aggregationKey;
  const user = await sdk.enrichWith('userProfile', { userId });
  return [{ id: user.id, title: `Your ${reached}th order — thank you!` }];
};
```

## Inspect & simulate

Open a synthetic event to see its **live activity** — what's accumulating right now, grouped by environment — and act on it from the dashboard:

* **Scheduled** — see the next fire times and **Fire now** to simulate it immediately (handy to test the campaign without waiting for the hour).
* **Grouped** — see each summary that's building up (per user, per dimension) with its event count; **Send now** flushes one immediately, or **Discard** drops it.
* **Reminder** — see armed timers and when they'll fire; **Fire now** triggers one early, **Cancel** disarms it.
* **Milestone** — see each counter's current value vs the next threshold; **Reset** zeroes it.

Anything you fire from here lands in the **Outbox** like a normal run, so you get the full debug loop: build it up → fire it → watch the workflow.

## How synthetic events appear

Once created, a synthetic event shows up in your **Events** list with a type badge, exactly like SDK or connector events. A campaign subscribes to it the same way it subscribes to any event. Until a campaign listens on it, the event simply fires and nothing happens — that's the normal Event ↔ Campaign separation, not an error.

## Privacy

Grouped and Reminder events accumulate activity for the length of their window before firing. That accumulation is held only for the window's duration, is scoped to the feature, and is never persisted to long-term storage or logged. You can restrict exactly which event fields are kept. As always, Notifizz holds **no customer data** beyond what your configuration needs for the window. See [Privacy friendly](/docs/concepts/privacy-friendly).

## FAQ

<AccordionGroup>
  <Accordion title="My Scheduled event fired a few minutes late — is that a bug?">
    No. Scheduled events are guaranteed within 5 minutes of their configured time. Treat the time as "around then", not to the second.
  </Accordion>

  <Accordion title="Do I need to write any code to create one?">
    No. Synthetic events are created and configured from the dashboard. You only write orchestrator code for the campaign that reacts to them — the same code you'd write for any event.
  </Accordion>

  <Accordion title="Can I tell synthetic events apart from real ones in my orchestrator?">
    Yes. Every synthetic event includes `event.properties._synthetic` with its `kind` and the id of the configuration that produced it. Real SDK events don't carry that marker.
  </Accordion>

  <Accordion title="What happens if no campaign listens on it?">
    The event fires and is recorded, but produces no notification. Create a campaign that subscribes to the event to act on it.
  </Accordion>
</AccordionGroup>
