Skip to main content
For events that aren’t part of the canonical lifecycle set, use events.track(). The name is accepted as-is — the backend validates only that it’s a non-empty string.

Single events

The first argument is the event name. The second is the external user ID. The third is an optional properties bag — anything JSON-serialisable.
The 10 canonical lifecycle names work through track() as well, but prefer the typed helpers in vf.users.* for autocomplete and type safety.

Naming conventions

Use dashboard_created, not DashboardCreated. Stable across audience filters and triggers.
Renaming an event breaks any audience filter that already references the old name. Treat event names like database column names — pick once, live with it.
Use workspaceId, not workspace_id. The backend normalises both to the same column, but consistency in your own code is worth holding the line on.

Batch tracking

For high-volume work — backfilling historical events, syncing from a data warehouse, replaying a Segment export — use events.batch() to amortise HTTP overhead:
The batch auto-flushes when:
  • The queue reaches maxSize events (default 100), or
  • maxAgeMs has elapsed since the first queued event (default 5000), or
  • You call flush() explicitly.
Always await batch.flush() before process exit. Lambda-style runtimes that freeze the event loop will lose any queue still in memory.

Trade-offs

Batches are not idempotent across drains — if the POST fails midway through retries, the batch may be partially accepted server-side. Re-emit conservatively or set a retention-policy filter on your end to dedupe by event ID.

Reserved names

Reserved-name routing is an API-side concern, not the SDK’s. Custom names that happen to collide with a future reserved name will be rejected at validation with ValidationError — you’ll know immediately.
The current reserved set is the 10 canonical lifecycle names. Treat anything Vibefollow ships with as reserved.