# Conversation data format

> The universal format for conversations, reviews, surveys and tickets, column by column, with what breaks each one.

One row is one message. A conversation is one chat, ticket, call, review or survey response, and its rows share a `sessionId`. A review is a one-message conversation; a chat or ticket is several rows.


## Columns

| Column | Required | Type | Example | Notes |
|---|---|---|---|---|
| `text` | Yes | text | `My order never arrived` | The words to enrich. Must not be empty on any row. |
| `userId` | Yes | text | `cust-4471920` | Who sent the message. Must not be empty: if you don't have one, copy the `sessionId` into it (the file checker does this for you). For a bot conversation, use the end user's id on both their messages and the bot's ([the userId rule](/data/sessions-and-conversations#the-userid-rule)). Never an email address, phone number or name: use your tool's internal id. |
| `incoming` | Yes | `true` or `false`, lowercase | `true` | `true` when the customer or end user wrote it; `false` for your agent, bot or company. |
| `dimensionlabs_timestamp` | Yes | number, Unix time in **milliseconds** (13 digits) | `1703240320000` | When the message was sent. |
| `sessionId` | Yes | text | `1434868331` | Groups the rows of one conversation. Enrichment reads all the rows with the same `sessionId` at once. No colons, and no personal data. |
| `conversationId` | Recommended | text | `conv-88213407` | Keeps a multi-participant thread together, for example when a human agent joins a bot chat. No colons. |
| `intent` | Only for an intent-based chatbot | text in a CSV; an object in JSONL | `TRACK_ORDER` | The intent your bot matched. Leave it out if your bot doesn't use intents. |
| any other column | Optional | anything | `store_id`, `rating`, `plan` | Kept with the conversation as metadata. You map it during upload, and it appears in the `structured_metadata` table, where the agent can filter and join on it. Leave out emails, phone numbers, names and addresses ([PII redaction](/data/pii-redaction)). |

Column names are case-sensitive: `userId` and `sessionId`, not `userid` or `SESSIONID`. The upload screen's column mapping lets you point your own column names at these fields, and the [converter](/data/convert-a-file) renames them for you. Uploads accept CSV, TSV and JSONL files of any size: Dimension Labs splits a large file for you.

## Rules that break uploads

These are the problems behind most failed uploads. The [file checker](/data/check-a-file) finds every one.

- **Empty text.** Any row with an empty `text` cell. Remove the row.
- **Booleans in capitals.** `TRUE` and `FALSE`, or a mix of cases. Use lowercase `true` and `false`.
- **Timestamps in seconds.** A 10-digit number like `1703240320` is read as a date in 1970. Multiply by 1,000.
- **Excel's scientific notation.** Excel rewrites long numbers as `1.70324E+12` when it saves a CSV, and the digits it drops are gone. Re-export from the source system, or give the checker the original `.xlsx`.
- **Newlines inside a text field.** A line break inside a review or message can split one record into several. Replace line breaks with spaces.
- **Every field in quotes.** Some tools write every cell wrapped in quotes, including numbers and empty cells. Write the file with quotes only where needed.
- **Colons in ids.** `sessionId` and `conversationId` can't contain `:`. Use `-` or `_`.
- **A byte order mark, or Windows line endings.** Save as UTF-8 without a BOM, with Unix (LF) line endings.
- **A header with a backslash or a trailing space.** `Data Source ` with a trailing space becomes a different column from `Data Source`.
- **No header row.** The upload needs column names in the first row. Add one in a text editor.

## Small rules that keep results clean

- Keep messages in the same conversation at least one second (1,000 ms) apart.
- Don't use placeholder text such as `[No review text]` for an empty review. Drop the row instead.
- Pick one encoding for booleans and dates in your extra columns, and stick to it. Everything you upload is stored as text, so `Yes`, `yes` and `1` become three different values.

## Example

```csv
sessionId,userId,incoming,text,dimensionlabs_timestamp,channel
conv-1001,cust-17,true,My order never arrived,1703240320000,chat
conv-1001,cust-17,false,Sorry about that. Can you share the order number?,1703240325000,chat
conv-1001,cust-17,true,DM34137KMX,1703240331000,chat
review-77,rev-77,true,Great staff but the wait was 40 minutes,1703278090000,google_reviews
```

The agent's reply in `conv-1001` carries the customer's id, `cust-17`. Download it as a template: [conversation template](/templates/conversation-template.csv). Then check your own file with the [file checker](/data/check-a-file).

## A JSONL example

A JSONL file has one JSON object per line, with no commas between lines and no surrounding brackets. A line has the same shape as a [universal API](/api/tracker#the-message) message: `intent` is an object, and extra fields go in a nested `platformJson` object. This example file holds two bot conversations: a customer asking how to track a delivery, and a customer whose order number isn't found and who asks for a person.

```jsonl
{"text":"hey","userId":"7675836189527716","incoming":true,"sessionId":"1434868331","conversationId":"1434868331","dimensionlabs_timestamp":1703240310000,"intent":{"name":"HELLO","confidence":0.96},"platformJson":{"customerTier":"silver","locale":"en-CA"}}
{"text":"Hi! How can I help you?","userId":"7675836189527716","incoming":false,"sessionId":"1434868331","conversationId":"1434868331","dimensionlabs_timestamp":1703240315000,"intent":{"name":"HELLO_RESPONSE","confidence":1},"platformJson":{"customerTier":"silver","locale":"en-CA"}}
{"text":"I want to know where to track my deliveries","userId":"7675836189527716","incoming":true,"sessionId":"1434868331","conversationId":"1434868331","dimensionlabs_timestamp":1703240320000,"intent":{"name":"TRACK_ORDER","confidence":0.91},"platformJson":{"customerTier":"silver","locale":"en-CA"}}
{"text":"DM34137KMX","userId":"1165873486259425","incoming":true,"sessionId":"9950934871","conversationId":"9950934871","dimensionlabs_timestamp":1703278090000,"intent":{"name":"ORDER_NUMBER","confidence":0.88},"platformJson":{"customerTier":"bronze","locale":"en-CA"}}
{"text":"Sorry, I can't find any existing order matching that number.","userId":"1165873486259425","incoming":false,"sessionId":"9950934871","conversationId":"9950934871","dimensionlabs_timestamp":1703278095000,"intent":{"name":"ORDER_NOT_FOUND","confidence":1},"platformJson":{"customerTier":"bronze","locale":"en-CA"}}
{"text":"Operator","userId":"1165873486259425","incoming":true,"sessionId":"9950934871","conversationId":"9950934871","dimensionlabs_timestamp":1703278100000,"intent":{"name":"REQUEST_HUMAN","confidence":0.97},"platformJson":{"customerTier":"bronze","locale":"en-CA"}}
{"text":"Okay, I'm getting someone from my team to help you.","userId":"1165873486259425","incoming":false,"sessionId":"9950934871","conversationId":"9950934871","dimensionlabs_timestamp":1703278105000,"intent":{"name":"REQUEST_HUMAN_RESPONSE","confidence":1},"platformJson":{"customerTier":"bronze","locale":"en-CA"}}
```

Things to notice:

- `userId` is the customer's id on the bot's messages too.
- `incoming` is a real boolean, `true` or `false`, without quotes.
- Timestamps rise within each conversation and are at least a second apart.
- `customerTier` and `locale` are extra fields in `platformJson`, kept as metadata the agent can filter on.
- The timestamp field is `dimensionlabs_timestamp`. Older examples used other names, such as `dimension_timestamp`, which don't work.
