Pushrail Docs
Open app
Guides

Transforms

Reshape event payloads per destination with declarative, versioned rules.

Transforms

A transform reshapes an event payload on its way to a destination. v1 transforms are declarative, a small set of operations (rename, drop, add, path-map, wrap) composed in a config object. Transforms are versioned and pinned per delivery attempt so replays are deterministic even after the transform definition changes.

What transforms can do (declarative v1)

The v1 transform language exists for one job: bend the canonical event into the shape a specific destination expects. It is intentionally bounded, no loops, no conditionals beyond filters, no I/O. Sandboxed scripted transforms ship in a future tier; see the Concepts entry on transforms.

A transform is attached to a destination. When a delivery for that destination is built, the transform runs over the canonical event payload and the result is what gets sent. The original canonical event is unchanged and always recoverable via the delivery log.

Renames

Map a source field name to a destination field name:

{
  "renames": [
    { "from": "payload.customerEmail", "to": "payload.email" },
    { "from": "payload.totalCents", "to": "payload.amount" }
  ]
}

Renames preserve the value and the original location is removed. Use this when the destination's schema uses different conventions (snake_case vs camelCase, etc).

Drops

Remove fields that should never leave Pushrail (PII the destination doesn't need, internal-only metadata):

{
  "drops": ["payload.internalNotes", "metadata.debugTrace"]
}

Drops happen after renames, so you can drop by either the original or renamed path.

Adds

Inject static fields the destination requires that the canonical event doesn't carry:

{
  "adds": [
    { "path": "payload.source", "value": "pushrail" },
    { "path": "payload.environment", "value": "production" }
  ]
}

Use adds for destination-specific envelope fields, version markers, or tenant-identifying constants.

Path maps

Lift a nested value to a top-level location, or vice versa, using JSON paths:

{
  "pathMaps": [
    { "from": "$.payload.customer.id", "to": "$.distinctId" },
    { "from": "$.payload.order.totalCents", "to": "$.properties.amount_cents" }
  ]
}

Path maps are the most common shaping operation when targeting analytics tools (PostHog distinctId/properties) or warehouses (flattening nested JSON into columns).

Versioning + replay determinism

Every transform definition is immutable once saved. Editing a transform produces a new version; old versions are kept indefinitely. Each delivery attempt records the transform version it used, so a replay always re-applies the same transform that ran originally, even if the destination's current transform has changed since.

This matters because transforms can change behavior in subtle ways. If a customer rolls forward to a new transform version, then asks you to replay last week's failures, the replay uses last week's transform, not today's. Determinism by default.

Where scripted transforms are heading

The roadmap includes a sandboxed JavaScript/TypeScript runtime for transforms that need loops, conditionals, or richer string manipulation than the declarative DSL allows. Each script runs in a secure, isolated sandbox with strict CPU and memory limits and no network or filesystem access. The scripted runtime keeps the same versioning and replay-determinism contract: scripts are immutable per version and pinned per attempt.

Scripted transforms are planned for the Growth tier and above. Until then, if your transformation needs exceed what the declarative operations cover, run the reshaping in your application code before calling events.emit, and tell us about the use case so we can prioritize the script runtime correctly.