SELECT that returns one row per event, with columns named user_id or email, event_name, timestamp, and event_id. Every other column becomes a property of the event.
How warehouse rows map to Conversion
A custom event records that a contact did something at a point in time. Unlike contacts and custom objects, events are never updated: each row adds one event to one contact.
Once synced, marketers can use the events in:
- Workflow triggers, with Custom event occurred
- Audience and query filters
- Lead scoring rules
Column reference
Conversion reads the column names your query returns, ignoring case. It doesn’t use field keys or a mapping screen for events.Set up a custom event warehouse sync
Before you begin
- A connected warehouse with a read-only user that can see the tables you plan to sync
- Admin access in Conversion to create syncs
- Your contacts already in Conversion, from a contacts sync or another source. An event sync only creates contacts if you turn on Create contacts for unmatched emails.
1. Agree on the events with your marketers
Before you write any SQL, agree with your marketers on which events to send and what they need from each one. Triggers, filters, and scoring rules match events by name and filter on their properties, so the names and properties you choose are what marketers will build with.
Write down each event name and its properties before you start. Keep names stable: renaming an event later means updating every trigger and filter that uses it.
2. Write the query
Return one row per event. Name the columns as in the column reference, and convert the timestamp to Unix seconds as text. This example uses Snowflake syntax:event_name instead of a fixed string:
last_sync_time syntax differ by warehouse. See Timestamps and your warehouse’s guide: BigQuery, Snowflake, ClickHouse, or Databricks.
3. Create the sync
- Go to Settings → CRM and syncing → Syncs.
- Click Add sync.
- Enter a Name and Description, and pick the Connection.
- For Object, choose Custom events.
- Set the Frequency. Events always use the Upsert mode.
- Turn on Create contacts for unmatched emails if events should create contacts that don’t exist yet. This requires an
emailcolumn. - Turn on Subscribe new contacts if the contacts this sync creates should be subscribed to email.
- Click Edit query, paste your query, and click Preview to check the first 100 rows.
- Click Create sync.
- Open the sync and click Activate.
4. Check the results
- Open the sync’s Logs tab and check the rows processed and any skipped or failed rows. See Common mistakes for what each reason means.
- Open a contact you expect to have events, go to its activity log, and filter to Custom events. Confirm the event name, time, and properties.
- Share the result with your marketers and confirm they can build the triggers, filters, and scoring rules you agreed on.
How contacts are matched
Each row must identify one contact withuser_id, email, or both.
- If the row has a
user_id, Conversion looks for the contact with that user ID. - If none is found and the row has an
email, Conversion looks for the contact with that email. - If the email matches a contact that has a different user ID, the row is skipped as Conflicting identifier.
- If no contact matches, the row is skipped as Contact not found, unless Create contacts for unmatched emails is on. Then Conversion creates the contact from the row’s email (and user ID, if present). Rows without a valid email are still skipped.
user_id when you have it, since it doesn’t change when a user updates their email.
Timestamps
Name the columntimestamp. Conversion stores it in UTC.
- Send it as text. A numeric value fails with Invalid timestamp, so cast it to a string in your query.
- Unix seconds (
1730073600) are the safest format. Unix milliseconds and ISO 8601 strings (2026-10-28T00:00:00Z) also parse. - If the column is missing or the value is null, the event is stamped with the time it was synced.
{{last_sync_time}} filters which rows a run reads. The timestamp column sets when each event happened. They can come from different columns: if events can land in your warehouse late, filter on the time the row was loaded and send the time the action happened as timestamp.
Duplicates and backfills
With anevent_id, Conversion keeps one event per ID. If a later run sends the same event_id again, for example because of a time buffer or a re-run, no second event is created. Repeats within three days show in the sync’s logs as Duplicate event. Without an event_id, every row creates a new event, so overlapping runs create duplicates.
An event_id must be unique across every event sync in your workspace, and across events sent through the Events API. Conversion doesn’t scope it by sync or event name, so if two tables both have a row with ID 123, one event silently replaces the other. Prefix the table’s primary key with the table or event name, such as 'order:' || o.id. If the table has no single key, build one that is stable across runs, such as the user ID, event name, and timestamp joined together.
On the first run, {{last_sync_time}} is 0, so the query returns all history. To start from a recent date instead, add a fixed lower bound to the WHERE clause.