deals table with forty columns named nothing like
Octave’s fields, and you need to know that a row with stage = 'Closed Won' is a
deal_won event, that owner has to split into an email and a name, and that one deal
row is usually more than one event.
This section walks that mapping, then the pipeline around it.
Where does your data live?
Amazon S3
Parquet, CSV or JSONL in a bucket, usually partitioned by date.
Google Cloud Storage
Same file-drop shape, read through the GCS client.
Azure Blob Storage
Containers and blob prefixes, with or without a hierarchical namespace.
Snowflake
Query-based extract with a modified-at watermark.
BigQuery
Storage Read API, parameterised incremental queries.
Amazon Redshift
Direct cursor for small pulls, UNLOAD for large backfills.
Databricks
SQL warehouse queries or Delta change data feed.
What kind of events do you have?
Octave takes four kinds of activity. Most warehouses have tables that map onto them directly:Only
sent and reply email events and transcript call events are processed for
analytics. All CRM and all social types are processed. Other email and call types are
accepted and stored, but produce no findings — sending them is not an error, it just
does not buy you anything.Backfill or continuous?
The answer decides which endpoint you POST to.
Run the backfill first and confirm
counts.ingested before you wire anything to a
schedule. Once the history is in, the same mapping code drives the incremental run: point
it at the webhook endpoints if you are reacting to individual rows, or keep using bulk
import if you are running batches on a timer. Bulk import is the better default for
anything scheduled — a nightly job pushing 4000 rows is four requests instead of four
thousand.
Where this runs
This is not something to run off a laptop. The pipeline needs to fire on a trigger or a schedule, hold a watermark between runs, and keep your credentials somewhere managed — so it belongs in a serverless function next to the data. Each source page names a concrete runtime, and the prompt on that page tells the agent to build and deploy for it:The shape of the pipeline
Whatever the source and whatever the runtime, the flow is the same:- Trigger — an object lands, or a schedule fires.
- Read incrementally, from a watermark held in a managed store.
- Map your columns onto the Octave event structures.
- Batch to 1000 events or fewer.
- POST to
/api/v2/event/import. - Poll the returned
jobOIduntil the job reportsCOMPLETED. - Advance the watermark, once the batch is confirmed ingested.
Mapping reference
Every event carries atype discriminator on bulk import. The per-type webhook endpoints
take it from the URL path instead, so it is the one field that differs between the two
transports.
Required fields, by type:
A CRM event with the fields worth sourcing if your table has them:
Conversion rules
The problems that actually come up when the input is a warehouse table rather than a webhook payload:One row can be more than one event
This is the rule most mappings miss. Adeals row with both created_at and closed_at
populated is two events, not one: an opportunity_created at the created timestamp, and a
deal_won or deal_lost at the closed timestamp. Each needs its own eventTimestamp and
its own eventId — suffix the row’s primary key so they stay distinct.
Collapse them into a single event and the deal appears in Octave with no history. The
milestones Octave reports on are recorded per event, so a deal that only ever arrives as
deal_won has no creation date to measure a sales cycle against.
Keeping deals current
SendcrmLastModifiedAt on every CRM event. A deal is one object that changes over time,
so the same opportunityId comes back repeatedly as the amount moves and the stage
advances. Without a last-modified timestamp an updated deal can look identical to the
previous push and be discarded as a duplicate. See
Keeping deals up to date for the full explanation.
Troubleshooting
You get a 400 and nothing at all imports. Validation is all-or-nothing per request — one bad row rejects the entire batch, not just itself. The response message lists the offending zero-based indices, so map them back to your batch and fix those rows. An invalid API key comes back as 404, not 401. A “workspace not found” response usually means the key is wrong or belongs to a different workspace, not that the endpoint path is wrong.counts.skipped is high. Skipped is not failed. Duplicates and event types that
aren’t processed for analytics both land there. counts.ingested is the authoritative
success count, and counts.failed is the one to alert on.
Old rows vanish without an error. Check the timestamp window. Anything more than 50
years in the past or 5 years in the future is rejected, which catches sentinel dates and
any column where a null got coerced to epoch zero.
Deals import but every pipeline chart is flat. You are almost certainly emitting one
event per row instead of one per milestone. See
One row can be more than one event.