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
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
Short answers here; the docs go deeper.
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 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
pnpm dlx create-hogsend@latest my-app