Transformation.
A stack of typed actions, in flight, between the gate and the destinations. Ten today, grouped into three categories: enrichment adds new fields, mutation rewrites them, control may drop the event. Same payload, any number of actions, one pass.
fig. 1 — one event walks three actions. Each tile fires as the token crosses it, writing a new field onto the persistent entity record. Entities outlive events.
In flight01
What happens between the gate and the wire.
A composable stack of typed actions. They run in the order you declare. Each is optional per event family: page views skip the stack entirely, signups walk the whole kitchen. The output of one action is the input to the next, all in the same typed event object.
- i.
- enrichment
- Add fields the event did not carry. Person and company profiles, LLM labels, summaries, translations. Five actions today.
- ii.
- mutation
- Rewrite fields in place. Per-destination PII redaction. A custom-template escape hatch for everything else. Two actions today.
- iii.
- control
- Decide whether the event continues. Dedupe drops repeats. Throttle rate-limits per key. Filter drops unless a predicate matches. Three actions today.
- +enrich.personemail → person profile
- +enrich.companydomain → company facts
- +classifyLLM labels with a typed schema
- +summarizeLLM short summary of long text
- +translatetranslate text to target language
- ~redact.piiper-destination PII matrix
- ~transformescape hatch · custom template
- ▽dedupedrop if seen recently
- ▽throttlerate-limit per key
- ▽filterdrop unless predicate matches
Order matters. Put dedupe ahead of classify and the model bill drops. Put redact.pii ahead of route and PII never reaches the destinations. The next three sections walk the most consequential actions in depth.
Classify02
The model meets the payload.
The model is yours. The prompt is yours. The labels are yours. We do the wiring: prompt assembly, structured response parsing, schema enforcement, retries, dead-letter on mismatch.
Labels are typed. Enums for categories, numbers for scores, free-form strings where they belong. The label schema is part of the pipeline YAML, which means it diffs in PRs and rolls back with git revert. A new label is a code change.
# pipelines/signup.yaml (excerpt)
steps:
- classify:
input: $event + $person
model: openai/gpt-4o-mini
prompt: |
Decide the intent of this signup.
labels:
intent: [casual, hot_lead, partner, spam]
priority: 0..100
cache:
key: $payload.email
window: 1hProvider keys never leave the region. The payload-hash cache pays for itself in a week on any high-frequency event family where the payload shape repeats.
Enrich03
Identity, attached.
Enrichment lives on the entity, not the event. Once a person, account, or session is resolved, every subsequent event for that entity inherits the context: name, plan, lifecycle stage, last invoice, last invite. You attach it once. We carry it.
Compute fields run alongside resolution. Counts, sums, time windows, ratios. These are spec, not code: declare what you want in the YAML, get it on every event.
- enrich.person:
from: $event.email
store: entities.people
- enrich.account:
from: $person.account_id
store: warehouse.accounts
- compute:
seats_used: $account.seats_used
invites_7d: count(
"team.member.invited",
window: "7d",
by: $account.id
)Redact04
Per-destination policies, enforced at the wire.
Redaction is the step the homepage will not soften: PII never leaves its lane. PostHog gets the event. PostHog does not get the email. The warehouse, in its own region, gets the full fidelity. Slack gets a paraphrase. Same event, three destinations, three different shapes on the wire.
Three operations per field: drop removes it, hash replaces it with a deterministic hash, mask keeps the type but blanks the value. Choose per field, per destination, per event family. Defaults are strict; you opt fields in, never out.
destinations:
- posthog:
redact:
- $person.email: drop
- $person.phone: drop
- $payload.ip: hash
- slack:
redact:
- $payload.ip: drop
- warehouse:
redact: [] # full fidelity, EU-onlyNext05
Where the typed event goes from here.
After three transformations, the event has labels, identity, computed fields, and a wire-ready shape per destination. The last step decides where it lands.
Ready to put a stack of typed actions between your events and your destinations?
sign upread the docs
