Custom events
A custom event is any event Chord receives that is not part of the Chord tracking plan — either an event whose name is not one of the canonical events (Order Completed, Product Viewed, Checkout Started, Product Added, …) or a canonical event carrying non-standard properties.
This document explains what happens to those events: how they are processed, what transformation (if any) is applied, and which destinations forward them.
The short answer
Chord does not validate events against the tracking plan, and it does not reject, quarantine, or strip events for being non-compliant. Every event that is ingested flows through the same pipeline. Custom events are processed as-is, with only the minimal, optional transformations described below, and are forwarded to destinations.
What a custom event ultimately looks like at a destination depends entirely on that destination, because each destination decides how to map an event to its own schema. Some forward custom events verbatim; some only act on a fixed set of canonical events and silently ignore everything else.
How an event is processed
Every event — canonical or custom — travels the same path:
Chord SDK / S2S → Ingest → Kafka → Rotor (function chain) → DestinationInside Rotor, the function chain applies a series of transformations. None of these steps inspect whether an event complies with the tracking plan. They treat a custom event exactly as they treat a canonical one:
Step | What it does | Applies to custom events? |
|---|---|---|
Geo enrichment | Resolves context.ip to context.geo (browser events only) | Yes, identically |
User recognition | Merges anonymous events with known identity (if configured) | Yes, identically |
Connection filters | Drop/allow rules matching on event type, name, or property values | Yes — can drop a custom event if a rule names it, but not because it is custom |
Property mappings | Customer-defined field remapping (@jitsu/mapping-engine) | Yes, identically |
Consent filtering | Drops events lacking required consent categories | Yes, identically |
Deduplication | Skips repeat messages within a Redis-backed window (if enabled) | Yes, identically |
UDFs | Custom JavaScript transforms that can modify, drop, or fan-out events | Yes, identically |
Destination function | Maps the event to the destination's API/schema | Destination-specific — see below |
The only places an event is ever dropped are: a connection filter rule, consent enforcement, a UDF returning null/"drop", deduplication, or a destination deliberately not firing. A custom event is never dropped merely for being absent from the tracking plan.
How destinations handle custom events
This is where behavior diverges. There are two delivery modes, and within each, destinations fall into a few distinct patterns.
Cloud-mode destinations (server-side)
These run as functions in Rotor. The dominant pattern is passthrough: the event name and properties are forwarded with little or no transformation, so custom events arrive intact.
Pattern | Behavior for custom events | Examples |
|---|---|---|
Passthrough | Event forwarded as-is; the event name is used directly | Webhook, Segment, MongoDB, Data Warehouse / Bulker, June, Intercom, Heap, Mixpanel, PostHog |
Canonical mapping + fallback | Canonical events are translated to the destination's standard event; unmapped (custom) events still go through via a default fallback | GA4 (custom name is handelized/kebab-cased, standard props filtered), Facebook Conversions (custom name passed through unchanged) |
Configurable mapping | The destination acts only on events the customer has explicitly mapped | Amplitude (supports a $drop mapping), Braze (purchase-event list, otherwise generic track) |
Identity / fixed-purpose | Built around identify/group or a small set of events; track-style custom events get generic treatment or none | Klaviyo (special-cases Order Completed, all other tracks become generic metrics), HubSpot, Salesforce |
Data warehouse destinations are the most permissive: a custom track event simply gets its own table named after the event (e.g. my_custom_event), or its raw document inserted (MongoDB).
Device-mode destinations (client-side)
Device-mode plugins (libs/jitsu-js/src/destination-plugins/) fire directly from the browser via each vendor's pixel/SDK. Per the forwarding policy above, every plugin handles the unrecognized-event case explicitly — it either forwards the event or logs an explicit skip.
Pattern | Behavior for custom events | Examples |
|---|---|---|
Passthrough | An unrecognized event name is forwarded to the vendor SDK under its own name (or as a generic "custom event") | Facebook Pixel (trackSingleCustom), TikTok Pixel, Pinterest Tag, Insider Pixel, Bing Ads, StackAdapt Pixel, Dotdigital Tag (event()), Google Tag Manager (pushed to the Data Layer as event: "<Event Name>"; triggers and tags decide what happens next) |
Fixed-taxonomy (explicit skip) | Only the platform's recognized events fire; unrecognized events are skipped with a console.warn | Snap Pixel, Northbeam Pixel, Reddit Pixel, Retention.com, Postie, Spotify Ad Analytics Pixel, OpenAI Ads Pixel and Conversions API, Nosto Tag and Nosto |
Config-driven | Fires only for events the customer mapped in the plugin's configuration; everything else (including unmapped canonical events) is skipped | Twitter Ads (eventMapping), Google Ads (clickConversions / pageLoadConversions) |
Custom property mappings
Custom property mappings are a separate mechanism from custom event names. When a customer maps a property in Hub (e.g. properties.customer_type → customer_type), the dispatcher writes it as a top-level key on the event. A plugin only carries it into the outgoing payload if the plugin spreads getCustomMappedProperties / getCustomDataProperties — in which case the custom property reaches the destination even on a fixed event set.
Not every plugin does this. Plugins that build a hardcoded payload from formatters (formatOrderData, etc.) without that spread silently drop custom-mapped properties. Today the plugins that forward them are facebook-pixel, snap-pixel, tiktok-pixel, pinterest-tag, twitter-ads, bing-ads, google-ads, reddit-pixel, and dotdigital-tag (on its custom-event call only — see below); fixed-taxonomy plugins such as spotify-ad-analytics-pixel, postie, retention, northbeam-pixel, sfmc-collect, thetradedesk-pixel, and yahoo-pixel do not. (PII fields like email and phone are excluded from this passthrough regardless.)
Forwarding is not the same as being used. reddit-pixel forwards custom properties and Reddit accepts them without error, but Reddit publishes a fixed metadata list (conversion_id, currency, item_count, value, products) with no general-purpose container, and does not document what becomes of anything else. So the property reaches the vendor and may still be inert. Where a vendor neither documents a passthrough field nor rejects unknown keys, say so in the destination doc rather than implying the mapping works — an operator who builds reporting on a silently-discarded property is worse off than one who was told nothing.
A closed vendor schema can make forwarding impossible, not merely absent. openai-ads-pixel is the first plugin in this category. OpenAI validates the event against a fixed schema — the conversion data accepts only type, amount, currency, contents and plan_id, and there is no custom_data slot like Meta's or TikTok's. An unknown key is not ignored: the SDK logs validation failed; event dropped and the conversion never leaves the browser.
So for that destination the convention inverts. It exists to stop custom properties being silently lost; here, forwarding them silently loses the entire event, which is strictly worse. The plugin therefore sends none, and its custom-property-mapping.test.ts asserts the exclusion so the exception reads as a decision rather than an oversight.
A closed vendor API can make forwarding impossible, not merely absent. nosto-tag is the second plugin in this category and the clearer case: Nosto's browser API has no property bag at all. Each method takes a fixed signature, and its cart, customer and order objects are closed shapes — there is no custom_data like Meta's, no properties like TikTok's, and no key Nosto would read. A custom property has nowhere to go on either Nosto surface, so both skip it and both public docs say so rather than leaving an operator to discover it.
A vendor can have an extensibility field on some calls and none on others. dotdigital-tag is the case that does not fit either bucket cleanly. Dotdigital's event() takes an arbitrary attribute bag, so custom properties are forwarded there — but every other method it exposes (identify, productBrowse, productList, cartUpdate, checkout, purchaseComplete) takes a fixed signature with no property bag, so they get none. The plugin therefore spreads on exactly one call, and its custom-property-mapping.test.ts asserts both halves: that mapped properties arrive on the custom-event path, and that they cannot appear on a commerce payload where Dotdigital defines the field names itself. The public doc states the split, because an operator configuring a mapping needs to know which events it reaches.
Before adding the usual spread to a new plugin, check whether the vendor has an extensibility field at all — and check it per call, not per vendor. If it does not, omit the spread and pin that with a test.
Custom event forwarding policy
Across destinations, the behaviors above are not all intentional — some are genuine vendor constraints and some are implementation gaps. To keep current and future destinations consistent, Chord follows one rule:
A silent fall-through (a switch with no default) is the thing to avoid: it drops events with no signal in the logs, so a custom event that never arrives at a destination looks identical to one that was delivered. That is the worst outcome for debugging.
Two buckets
Each destination belongs to one of two buckets, chosen by what the receiving platform actually does with an unrecognized event — not by whether the event can be transmitted:
Bucket | Default for unrecognized events | Why |
|---|---|---|
Passthrough-capable — general-purpose collectors and platforms with a real custom-event API | Forward the event under its own name (with custom-mapped properties) | The platform stores or acts on arbitrary events, so forwarding is useful |
Fixed-taxonomy — attribution pixels whose backend only recognizes a closed set of events | Skip with an explicit console.warn | The platform discards or rejects unrecognized events, so forwarding would be a no-op that falsely reads as "delivered" |
Examples of passthrough-capable: Webhook, Data Warehouse / Bulker, MongoDB, Mixpanel, PostHog, GA4, Facebook (pixel via trackSingleCustom / Conversions API via a custom event_name), TikTok, Pinterest, Insider, Bing, Dotdigital (the Tag's event() and the v3 events import both create an Insight data collection for any non-reserved name).
Examples of fixed-taxonomy: Snap (closed enum + five CUSTOM_EVENT_n slots), Spotify Ad Analytics (view/purchase/lead only), Postie (Snowplow schemas it has registered), Retention.com (fixed case-sensitive "Reclaim" names), Northbeam (custom goals must be pre-registered vendor-side), Nosto (a closed set of page types plus cart, customer and order updates, with no custom event at all), Reddit (eight standard events; a Custom event is discarded unless its name is registered in the advertiser's Reddit account), OpenAI Ads (closed enum plus a custom type whose name must be registered in Ads Manager).
Some fixed-taxonomy platforms offer a registered custom event as an escape hatch, which is fixed-taxonomy and config-driven at once. OpenAI Ads is the clearest example: it accepts a custom event type, but only for names the advertiser has already created in Ads Manager, and it silently discards any other name. So the destination skips an unmapped event by default, and forwards it only when the operator has supplied a Custom Event Mapping entry — an explicit statement that the name exists vendor-side. Forwarding on a guess would look like delivery and be a no-op.
Reddit has the same Custom escape hatch vendor-side, but Chord's Reddit destinations expose no event-mapping credential, so they always skip an unrecognized event rather than guess at a registered name.
Decide by platform, not by transport
A destination's bucket is a property of the platform, and the pixel (device-mode) and Conversions-API (server-side) implementations for the same platform must make the same choice.
Server-side / CAPI delivery is permissive by construction — event_name is just a string field in an HTTP body, with no client SDK enum to satisfy — so it is tempting to forward everything server-side. But transmitting an event the platform ignores is still a no-op; it just happens over HTTP instead of a JS call. Facebook accepts custom events on both its pixel and its Conversions API, so both forward. A fixed-taxonomy platform like Snap would skip on both. There is no case where the CAPI side should forward what the pixel side drops for the same platform.
OpenAI Ads shows why this is not merely tidy. Its two surfaces deduplicate on (pixel ID, event name, event ID), so they must agree on the name as well as the decision to send: if the pixel skipped an event that the Conversions API forwarded under a name of its own, the conversion would be recorded once under a name that matches no conversion goal — worse than dropping it, because it reads as delivered. Both surfaces therefore resolve event names through one shared table and honor the same Custom Event Mapping.
Practical guidance
- Sending a custom event to a warehouse or a passthrough destination? It will arrive with its original name and properties. No tracking-plan compliance is required.
- Sending a custom event to an ad platform via a device-mode pixel? Confirm the plugin is passthrough or that you have mapped the event in the plugin's configuration. Fixed-taxonomy plugins will skip it and log a console.warn.
- Want a custom event to look like a canonical one at a destination? Use a property mapping or a UDF in the connection to reshape it before it reaches the destination function.
- Want to stop a custom event from reaching a destination? Add a connection filter rule, or drop it in a UDF — there is no automatic rejection to rely on.
Summary
Chord is a flexible, schema-less pipeline. It accepts any well-formed event, transforms it only as explicitly configured, and forwards it. The tracking plan provides canonical names that destinations *prefer* when present, but compliance is never enforced. Custom events are first-class citizens of the pipeline; the only meaningful variation is how individual destinations choose to map — or ignore — events they were not built to recognize.