CSV, XLSX and Parquet upload
What the CSV, XLSX and Parquet upload connector reads, with which permissions, what it never touches, how it syncs, and the facts it produces.
Last changed Markdown versionllms.txt
Source module: app/connectors/generic_csv.py. Kind: both. Display name: "CSV / XLSX / Parquet upload". Auth: upload (no external system).
Purpose
Ingests any vendor's claims export and any facts export that has no built-in connector, using a saved column mapping. This is the path for Decagon, Sierra, Salesforce Agentforce usage, Zendesk automated-resolution exports, HubSpot Breeze resolutions, and for supplying conversation text to transcript assist.
Auth and scopes
None. Files are uploaded by an analyst or higher through POST /v1/connections/upload (multipart: file, kind = claims | facts, mapping JSON, optional save_mapping_as, optional connection_id to reuse an upload connection). POST /v1/connections/upload/preview returns headers, the first 10 rows, the row count and a suggested mapping. Uploads are limited to 50 MB. Claims uploads are checked against the plan's outcomes_per_month limit before ingestion.
Supported formats
| Extension | Parser |
|---|---|
.csv (any delimiter among `, ; tab |
`, sniffed; UTF-8 with or without BOM) |
.xlsx, .xlsm |
openpyxl (active sheet, first non-empty row = headers) |
.parquet |
DuckDB read_parquet |
Empty rows are dropped.
Mapping
mapping.columns maps canonical field → file header. Other mapping keys set defaults.
Claims (kind = claims):
| Canonical column | Required | Notes |
|---|---|---|
subject_id |
yes | Verbatim; must match the fact subjects it should be evaluated against (e.g. zendesk_ticket_123) or be linkable |
claimed_at |
yes | ISO-8601 (with Z, an offset, or a space instead of T), YYYY-MM-DD, MM/DD/YYYY [HH:MM[:SS]], DD/MM/YYYY, YYYY/MM/DD [HH:MM:SS], or epoch seconds/milliseconds; normalized to UTC. Naive values are treated as UTC. An unrecognized value aborts the upload with unrecognized timestamp: … |
claim_id |
no | If present, the stored id is `sha256(tenant |
outcome_type |
no | Default from mapping.outcome_type or resolution |
unit_price, currency |
no | Defaults mapping.unit_price / 0, USD |
channel |
no | For channel_in exclusions |
billing_period |
no | YYYY-MM; defaults to the month of claimed_at |
customer_email |
no | HMAC-hashed into customer_hid; never stored raw |
Mapping-level defaults: contract_id (default generic_outcome_csv), vendor (default generic), subject_kind (default conversation), outcome_type, unit_price.
Facts (kind = facts):
| Canonical column | Required | Notes |
|---|---|---|
subject_id |
yes | Verbatim |
occurred_at |
yes | Same formats as above |
fact_type |
yes (or mapping.fact_type default) |
e.g. customer_message, human_reply, status_change, reopen, escalation, csat_response, customer_confirmation, refund, credit, opportunity_stage, meeting_event |
subject_kind |
no | Default mapping.subject_kind or conversation |
from, to, actor |
no | Status change attributes (lower-cased) |
stage, status |
no | Opportunity/meeting attributes (lower-cased) |
score, sentiment |
no | CSAT; if sentiment is absent it is derived from score (≥4 positive, ≤2 negative, else neutral) |
amount |
no | Refund/credit amount |
intent |
no | For customer_message (confirmed_resolved satisfies customer_confirmed_resolved) |
text |
no | Stored only when mapping.keep_text = true; required for transcript assist |
customer_email |
no | HMAC-hashed into customer_hid |
Mapping-level: source (stored as the fact source, default generic_csv), provides (list of source categories this upload supplies, e.g. ["conversations", "csat"]; merged into the connection's config.provides and used for coverage disclosure and missing_fact_source).
Suggested mappings are produced from common header names (conversation_id, ticket_id, resolved_at, rating, body, …); see SUGGEST in the module.
What is hashed / never stored
customer_email→customer_hid(HMAC-SHA256, per-tenant key). The raw email is not stored in claims or facts.- Unmapped columns are ignored and never persisted in
claims/facts. - The uploaded file itself is landed in the raw landing zone (below) and deleted after
RAW_LANDING_RETENTION_DAYS(90). textis stored only withkeep_text = true, is excluded from the evidence API, is redacted before any transcript-assist call, and excerpts derived from it are purged after 30 days.
Raw landing, incremental behaviour, watermark
- Every upload is written to
tenants/{tenant_id}/generic_csv/{YYYY-MM-DD}/{filename}before parsing. - There is no incremental sync and no watermark:
sync()is a no-op andPOST /v1/connections/{id}/syncreturnsnot_syncable. Each upload is processed in full. Because ids derive fromsha256(tenant_id | csv:{filename}:{claim_id or row}), re-uploading the same file name updates the same rows; uploading the same data under a different file name creates new rows unlessclaim_id/fact_idcolumns are mapped (then ids derive from those values and stay stable across file names). - A
sync_runsrow is recorded per upload with row counts;connection.uploadis audited with the file name and counts.
Rate-limit handling
Not applicable.
Fact and claim types produced
Whatever the mapping declares. Claims always get raw_ref = csv:{filename}:{claim_id or row}; facts raw_ref = csv:{filename}:{fact_id or row}.
Subject id format
Whatever the file contains, verbatim. To be evaluated against facts from another connector, use that connector's format (intercom_conv_{id}, zendesk_ticket_{id}, sf_case_{Id}, hubspot_ticket_{id}, stripe_customer_{id}), or supply customer_email on both sides so hashed-email linking can connect them.
Known limitations
- 50 MB per upload; the file is parsed in memory.
- Parquet parsing writes a temporary file on the host.
- Only the active worksheet of an XLSX is read.
- Time zones: naive timestamps are treated as UTC.
- Rows with an empty
subject_id,claimed_at/occurred_at(or, for facts,fact_type) are skipped silently. A non-empty but unrecognized timestamp raisesunrecognized timestamp: …and aborts the whole upload; fix the column or the mapping and re-upload (the file lands in the raw zone before parsing, so nothing partial is persisted). - No de-duplication across differently named files unless
claim_id/fact_idcolumns are mapped.