Events are facts. Name them like facts.
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
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.
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
// 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.
// 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.
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.
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.
Questions, answered
The short versions. The docs have the long ones.
Go deeper
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. 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
pnpm dlx create-hogsend@latest my-app