Lunogram

Events

Track user and organization actions to trigger journeys, update lists, and power segmentation

Events are at the heart of Lunogram. Every meaningful action — a user signing up, a link being clicked, a subscription being upgraded — is recorded as an event. These events drive the rest of the platform: they trigger journeys, recompute list memberships, fire schedules, and provide the data that powers segmentation and personalization.

There are two kinds of events:

  • Custom events — actions you track from your application (purchases, feature usage, form submissions)
  • System events — actions Lunogram records automatically (user created, list membership changed, link clicked)

Both kinds work identically once recorded. They can trigger journey entrances, appear in list rules, and carry arbitrary data properties.


Tracking Events

Send events through the JavaScript SDK or the client API. Events are processed asynchronously — the API returns 202 Accepted immediately, and processing happens in the background.

User Events

POST /api/client/users/events

// Browser — identifier can be omitted if already set on the client
await lunogram.user.events.post([
  {
    name: "order.completed",
    data: {
      orderId: "ord_abc123",
      total: 149.99,
      currency: "USD",
      items: 3
    }
  }
])

// Node.js — include the user identifier explicitly
await lunogram.user.events.post([
  {
    name: "order.completed",
    identifier: [{ externalId: "user_123" }],
    data: {
      orderId: "ord_abc123",
      total: 149.99,
      currency: "USD",
      items: 3
    }
  }
])

// Send to all users matching a data filter
await lunogram.user.events.post([
  {
    name: "maintenance.scheduled",
    match: { plan: "enterprise" },
    data: {
      window: "2025-07-15T02:00:00Z",
      duration: "4h"
    }
  }
])
# Target a specific user by identifier
curl -X POST https://your-instance.com/api/client/users/events \
  -H "Authorization: Bearer pk_..." \
  -H "Content-Type: application/json" \
  -d '[
    {
      "name": "order.completed",
      "identifier": [{ "external_id": "user_123" }],
      "data": {
        "orderId": "ord_abc123",
        "total": 149.99,
        "currency": "USD",
        "items": 3
      }
    }
  ]'

# Target all users whose data matches a filter
curl -X POST https://your-instance.com/api/client/users/events \
  -H "Authorization: Bearer pk_..." \
  -H "Content-Type: application/json" \
  -d '[
    {
      "name": "maintenance.scheduled",
      "match": { "plan": "enterprise" },
      "data": {
        "window": "2025-07-15T02:00:00Z",
        "duration": "4h"
      }
    }
  ]'
FieldTypeRequiredDescription
namestringYesName of the event
dataobjectYesEvent properties
identifierarrayNo*Array of { externalId, source? } objects to identify the user
matchobjectNo*JSONB containment filter — the event is delivered to every user whose data contains the given key/value pairs

* Provide either identifier or match, but not both, they are mutually exclusive. identifier is required when the user cannot be inferred from context (e.g., server-side calls). In the browser, the SDK can include it automatically if a user has been identified.

The request body is an array — you can send multiple events in a single call. Each event is processed independently.

await lunogram.user.events.post([
  { name: "page.viewed", data: { path: "/pricing" } },
  { name: "cta.clicked", data: { buttonId: "signup" } },
])

Matching Users by Data

Instead of targeting a specific user by identifier, you can use the match property to deliver an event to every user whose data column contains the given key/value pairs. Lunogram uses PostgreSQL's JSONB containment operator (@>) under the hood, so any user whose stored data is a superset of the match object will receive the event.

await lunogram.user.events.post([
  {
    name: "feature.announcement",
    match: { plan: "pro", region: "eu" },
    data: { feature: "custom-domains", releaseDate: "2025-08-01" }
  }
])

The example above sends the feature.announcement event to every user whose data contains both plan: "pro" and region: "eu". Each matched user gets their own independent event, processed through the normal pipeline (journey evaluation, list recomputation, etc.).

match and identifier are mutually exclusive. Including both in the same event object returns a 400 Bad Request error.

Organization Events

POST /api/client/organizations/events

await lunogram.organization.events.post([
  {
    identifier: [{ externalId: "acme_inc" }],
    name: "subscription.upgraded",
    data: {
      previousPlan: "pro",
      newPlan: "enterprise",
      seats: 100
    }
  }
])

// Send to all organizations matching a data filter
await lunogram.organization.events.post([
  {
    name: "compliance.update",
    match: { region: "eu" },
    data: { regulation: "GDPR", deadline: "2025-09-01" }
  }
])
# Target a specific organization by identifier
curl -X POST https://your-instance.com/api/client/organizations/events \
  -H "Authorization: Bearer pk_..." \
  -H "Content-Type: application/json" \
  -d '[
    {
      "identifier": [{ "external_id": "acme_inc" }],
      "name": "subscription.upgraded",
      "data": {
        "previous_plan": "pro",
        "new_plan": "enterprise",
        "seats": 100
      }
    }
  ]'

# Target all organizations whose data matches a filter
curl -X POST https://your-instance.com/api/client/organizations/events \
  -H "Authorization: Bearer pk_..." \
  -H "Content-Type: application/json" \
  -d '[
    {
      "name": "compliance.update",
      "match": { "region": "eu" },
      "data": {
        "regulation": "GDPR",
        "deadline": "2025-09-01"
      }
    }
  ]'
FieldTypeRequiredDescription
namestringYesName of the event
dataobjectNoArbitrary event properties
identifierarrayNo*Array of { externalId, source? } objects to identify the organization
matchobjectNo*JSONB containment filter — the event is delivered to every organization whose data contains the given key/value pairs

* Provide either identifier or match, but not both — they are mutually exclusive. One of the two is always required.

Organization events can also trigger journeys. When they do, the journey entrance evaluates each member of the organization — optionally filtered by a user rule — and enters matching members into the flow. When using match, the event fans out to every matched organization and each one is processed independently.


System Events

Lunogram automatically tracks events as things happen across the platform. You don't need to send these — they're emitted internally and are available in journey triggers, list rules, and the event timeline.

User Events

EventWhen It Fires
user.createdA new user is identified for the first time
user.updatedA user's profile properties change

Organization Events

EventWhen It Fires
organization.createdA new organization is created
organization.updatedAn organization's properties change
organization.user.addedA user is added as a member
organization.user.updatedA member's role or properties change

List Events

EventWhen It Fires
list.user.addedA user enters a list (dynamic rule match or static addition)
list.user.removedA user leaves a list (no longer matches or manually removed)

List events are especially useful for triggering journeys. For example, you can start a winback flow when a user is removed from your "Active Customers" list, or send an onboarding sequence when they're added to a "Trial Users" list.

EventWhen It Fires
link.clickedA tracked link redirect is followed

Link click events include campaign_id and original_url in their data, letting you build segments or trigger automations based on which specific links users interact with.

Scheduled Events

EventWhen It Fires
scheduled.<schedule_name>A schedule fires at its configured time
scheduled.anniversaryBuilt-in schedule that fires on the anniversary of user/org creation

Scheduled events follow the pattern scheduled.<schedule_name>, where the name matches the schedule you configured. The scheduled.anniversary schedule is created automatically for every project.

Journey entrances can listen for scheduled events and apply an offset (e.g., trigger 30 minutes before or 2 hours after the scheduled time). See Step Types — Scheduled Entrance for details.


How Events Are Processed

When an event is received, Lunogram processes it through a pipeline:

  1. Record — The event occurrence is stored against the user or organization, with its full data payload
  2. Schema extraction — Property names and types are automatically discovered and stored, providing autocomplete in the rule builder and journey trigger UIs
  3. List recomputation — Any dynamic lists with rules that reference this event type are re-evaluated for the affected user
  4. Journey evaluation — Journey entrance steps that listen for this event are checked, and matching users are entered into the flow

Steps 2–4 happen concurrently through a fan-out pipeline. This means a single event can simultaneously update list memberships and trigger multiple journeys.

Auto-discovered schemas: You don't need to pre-define event properties. Lunogram automatically learns the shape of your event data and surfaces property names in the rule builder and journey trigger UIs. The more events you send, the richer the autocomplete becomes.


Events in List Rules

Dynamic lists can use event rules to segment users based on behavior. An event rule checks whether a user performed a specific event a certain number of times within a rolling time window.

User did "order.completed" at least 2 times in last 30 days
  WHERE .data.total >= 100

This creates a list of users who made at least 2 purchases over $100 in the past month. As new order.completed events come in, list membership updates automatically.

See Lists — Event Rules for the full syntax and available operators.


Events in Journeys

Events are the most common way to trigger journey entrances. When you configure an event-triggered entrance:

  1. Choose the event name to listen for
  2. Optionally add a condition to filter which events qualify (e.g., only orders over $50)
  3. Configure re-entry rules — whether users can enter the journey multiple times, and whether they can have concurrent runs

When the event fires and the condition matches, the user enters the journey. The event data is available to all downstream steps through the entrance's data key.

journey.<entrance_data_key>.<property>

For example, if your entrance listens for order.completed with data key order, a send step can personalize the email with journey.order.total or journey.order.orderId.

See Journey Data for more on accessing event data in steps.


Event Data

Every event can carry an arbitrary JSON data object. This data is:

  • Stored with the event occurrence, visible in the user or organization timeline
  • Available in journeys through the entrance data key
  • Queryable in list rules using dot notation (e.g., .data.total >= 100)
  • Schema-discovered automatically, populating autocomplete in the UI

There's no need to register event names or define schemas upfront. Send any event with any properties, and Lunogram handles the rest.

Naming Conventions

Event names are case-sensitive strings. A few recommendations:

  • Use a consistent format across your application (e.g., order.completed, page.viewed)
  • Avoid names that collide with system events (anything starting with user., organization., list., link., or scheduled.)
  • Keep names descriptive — they appear in the journey builder, list rule editor, and event timeline

On this page