Hogsend is brand new.Try it
Hogsend
Guide: event naming

One way to name events, written down

context.object_action — lowercase, snake_case, past tense, one dot. The convention every Hogsend event already follows, written down so your whole stack can follow it too.

Free to self-host · One scaffold command · No per-contact billing

The problem

Sign Up, signup, user_signed_up, and userSignedUp are four names for one action. Six months in, every funnel needs a translation table, nobody trusts the chart, and the fix costs a break in event history. Naming is the cheapest piece of analytics infrastructure you'll ever ship — and the most expensive one to retrofit.

The convention

One pattern, defined once

Names are fixed strings in an as-const map; call sites import the constant. A typo in a string literal is an event that silently never matches a journey trigger — a typo in a constant is a compile error.

src/journeys/constants/index.ts
// src/journeys/constants/index.ts
// context.object_action — lowercase, snake_case, past tense, one dot.
export const Events = {
  // docs site
  DOCS_SUBSCRIBED: "docs.subscribed",
  DOCS_PAGE_VIEWED: "docs.page_viewed",
  DOCS_DEPLOY_CLICKED: "docs.deploy_clicked",

  // product lifecycle
  USER_CREATED: "user.created",
  TRIAL_STARTED: "trial.started",
  SUBSCRIPTION_STARTED: "subscription.started",

  // emitted by the Hogsend engine — same convention
  EMAIL_OPENED: "email.opened",
  EMAIL_LINK_CLICKED: "email.link_clicked",
  JOURNEY_COMPLETED: "journey.completed",
} as const;

The whole convention in one file: one dot of context, object before verb, past tense, lowercase snake_case. Engine-emitted events follow the same pattern as yours.

capture.ts
// One event, many properties — variance never goes in the name.

// Wrong: one event definition per docs section, forever.
capture(`docs.${section}_page_viewed`);

// Right: one event; the section is a dimension you filter on.
capture("docs.page_viewed", { section: "api", slug: "api/events" });

// "Viewed 3+ API reference pages" is now ONE filter on ONE event —
// and a Hogsend bucket criterion, not a union of twelve names:
b.event("docs.page_viewed")
  .where("section", "eq", "api")
  .within(days(30))
  .atLeast(3);

The most expensive mistake in event design is variance in the name. One event with a section property is one filter in PostHog and one bucket criterion in Hogsend; twelve interpolated names are a union nobody maintains.

The rules

Six rules, no exceptions

Short enough to fit in a code review comment. Strict enough that two engineers naming events a year apart produce the same name.

One dot of context

docs., email., trial., billing. — the dot scopes the event to the system that produced it, so names sort together, filter together, and never collide with whatever your stack invents next year. One dot only; billing.invoice.payment.failed is a hierarchy looking for a problem.

Past tense, on purpose

An event is a record of something that happened — deploy_clicked states a fact, deploy_click reads like a command. PostHog's guide says present tense; Segment's Object–Action framework says past. We side with Segment, and the engine already agrees: email.opened, journey.completed, trial.started.

Object before verb

deploy_clicked, not clicked_deploy. Noun-first puts every event about the same thing next to each other in an alphabetised dropdown — all the page_* events together, all the payment_* events together. Verb-first groups by interaction type, which is never the question.

A closed verb list

viewed, clicked, copied, submitted, started, completed, failed, created, updated, deleted, entered, left — pick the verbs once and never improvise. The point is that tapped, pressed, and clicked never coexist. A new verb is a code-reviewed decision, not a vibe at the call site.

One event, many properties

A name is an identity; a property is a dimension. Interpolating values into names — docs.api_page_viewed, docs.cli_page_viewed — produces hundreds of definitions that can't be grouped or graphed together. Fixed name, variance in properties, always.

Same action, same name, everywhere

If a deploy click is captured in the browser and forwarded to your lifecycle engine, both events are docs.deploy_clicked. The distinction worth keeping isn't transport — it's interaction events (a form was submitted) versus domain events (a subscriber now exists). The gap between those two is the funnel.

Try it

Paste a name, get a verdict

The check runs locally, against the rules above. The result is captured as docs.name_checked — the tool follows the convention it checks.

PostHog recommends present-tense verbs; Segment's Object–Action framework — the older, more widely adopted convention — recommends past tense, because an event is a record of a completed action. Hogsend's engine events were past tense before the convention was written down (email.opened, journey.completed), so present tense would mean fighting the platform. Both guides agree on everything else: lowercase, snake_case, never interpolate.

One convention for every event

The scaffold ships the constants file, the journeys that consume it, and the engine events that already follow the same pattern.

Free to self-host · One scaffold command · No per-contact billing

terminal
pnpm dlx create-hogsend@latest my-app