The toolkit
One configurable script, the toolkit, that turns CSV, TSV, JSONL, JSON or XLSX exports into upload-ready files, with a report of every row it dropped or fixed.
One-off conversion scripts are easy to write and hard to reuse: column positions, sheet names and file paths end up hard-coded, and next month’s export needs a new script. The toolkit, to-dimensionlabs.mjs, keeps everything that changes in a small JSON mapping file. You write the mapping once per export type and rerun it every time.
- Download the script: to-dimensionlabs.mjs
- Download an example mapping: to-dimensionlabs.example.json
For a quick one-time conversion without code, the in-browser converter may be all you need. Use the toolkit when the conversion repeats, or you need HTML stripping, skipped system rows, transcript splitting, a sheet other than the first, a header that isn’t in row 1, a time zone the converter doesn’t list, nested JSON paths or split output.
What it does
Section titled “What it does”- Reads CSV, TSV, JSONL, JSON or XLSX (the format is picked from the file extension).
- Maps your columns to
text,userId,incoming,sessionId,conversationIdanddimensionlabs_timestamp, and keeps the other columns as metadata (or the ones you list). - Sets
incomingfrom value lists ("customer"istrue,"agent"and"bot"arefalse), or a constant. - Fills a blank
userIdwith thesessionId, and gives the agent’s or bot’s replies the customer’suserId, so both sides of a conversation carry the end user’s id. - Reads timestamps as Unix milliseconds, Unix seconds, ISO 8601, clock times in a named time zone, or Excel serial dates.
- Replaces colons in ids, flattens line breaks in text, optionally strips HTML, drops rows with empty text.
- Splits transcripts with a speaker pattern, and reshapes wide surveys into question and answer rows, taking the question wording from the export’s own wording row.
- Removes duplicate messages, keeps messages in a session at least 1,000 ms apart, and splits output into parts of about N rows without cutting a session.
- Writes a report: rows in, messages out, every dropped row counted by reason, every fix counted, and a note when a kept column looks like personal data.
It writes CSV by default, or JSONL. A JSONL line has the same shape as a universal API message: extra fields go in a nested platformJson object and intent is an object ({ "name": "...", "confidence": 0.94 }). Uploads accept that shape, so either format works for a file upload.
Install and run
Section titled “Install and run”Node 18 or later.
npm install papaparse # alwaysnpm install xlsx # only if you read .xlsx filesnode to-dimensionlabs.mjs --config mapping.json --input export.csv --out out/export--input and --out override input.path and output.path in the mapping. Output files are <out>-1.csv, <out>-2.csv and so on, plus <out>.report.json. The script holds the whole input in memory; for an input larger than your machine’s memory, split it first or ask support.
Example: a help-desk export
Section titled “Example: a help-desk export”Example input, lumen-tickets.csv:
ticket_id,author_id,author_type,body,created_at,store_name,customer_email,customer_phone,priority4471,c-88,end-user,"<div>My card was charged twice<br>for one flat white.</div>",1/3/2026 9:12 AM,Portland Pearl,[email protected],555-0100,high4471,a-2,agent,Refund is on its way.,1/3/2026 9:20 AM,Portland Pearl,[email protected],555-0100,highThe example mapping, to-dimensionlabs.example.json. Keys that start with _ are notes and are ignored:
{ "_about": "Example mapping for to-dimensionlabs.mjs: a Lumen Coffee help-desk export, one row per message. Keys that start with _ are ignored. Docs: https://docs.dimensionlabs.io/transform/toolkit",
"input": { "format": "auto", "_format": "auto picks from the extension: .csv, .tsv, .jsonl, .json, .xlsx", "sheet": "Messages", "headerRow": 1, "skipRowsAfterHeader": 0, "_skipRowsAfterHeader": "survey tools often add a row of question text under the column names: set 1 to skip it (Qualtrics adds two: set 2)" },
"shape": "messages", "_shape": "messages = one row per message; transcript = one row per conversation with the whole transcript in a cell; survey = one row per respondent with one column per question",
"columns": { "text": "body", "userId": "author_id", "sessionId": "ticket_id", "conversationId": "ticket_id", "timestamp": "created_at", "_columns": "source column names; use a dot path such as sender.id for nested JSON. Never use an email, phone number or name as userId or sessionId. Optional: intent and intentConfidence, for an intent-based chatbot" },
"userIdForReplies": "customer", "_userIdForReplies": "customer (default) gives the agent's and bot's replies the end user's userId, so both sides of a conversation carry the customer's id; own keeps each row's userId",
"incoming": { "column": "author_type", "true": ["customer", "end-user"], "false": ["agent", "bot", "system"], "_incoming": "values are matched case-insensitively; rows with any other value are dropped and counted. Use \"incoming\": true when every row is from the customer" },
"timestamp": { "format": "auto", "timeZone": "America/Los_Angeles", "dayFirst": false, "_timestamp": "auto reads Unix ms, Unix seconds, ISO 8601 and Excel serial dates. timeZone applies to times written without a zone, such as 1/3/2026 9:12 AM" },
"clean": { "flattenNewlines": true, "stripHtml": true, "replaceColonsWith": "-" },
"metadata": { "keep": "unmapped", "drop": ["customer_email", "customer_phone"], "rename": { "store_name": "store" }, "_metadata": "keep: unmapped (every column not used above), none, or a list of column names. Drop personal data you don't need" },
"constants": { "channel": "email" },
"skip": [ { "column": "author_type", "values": ["system"] } ], "_skip": "rows whose column holds one of these values are dropped and counted, for example automatic ticket-closed notes",
"dedupe": true, "spaceMessagesMs": 1000,
"transcript": { "_used_when": "shape is transcript", "column": "transcript", "speakerPattern": "^(Agent|Customer):\\s*", "incomingSpeakers": ["Customer"], "outgoingSpeakers": ["Agent"], "secondsApart": 1, "userId": "session" },
"survey": { "_used_when": "shape is survey", "questionsFromRow": 2, "_questionsFromRow": "the file row (counting from 1) that holds the question wording; it must be one of the rows skipped by input.skipRowsAfterHeader. A question's own text, if set, wins", "questions": [ { "column": "why_score", "text": "Why did you give that score?" }, { "column": "improve", "text": "What one thing should we improve?" } ] },
"output": { "format": "csv", "path": "out/lumen-tickets", "chunkRows": 50000, "_output": "csv or jsonl. chunkRows 0 writes one file; otherwise files of about that many rows, never splitting a session" }}node to-dimensionlabs.mjs --config to-dimensionlabs.example.json --input lumen-tickets.csvRows in: 3 Messages out: 2 Sessions: 1 Dropped: 1 dropped 1: skipped: author_type is system fixed 1: html removed from text fixed 1: line breaks in text replaced with spaces fixed 1: replies given the customer's userId wrote out/lumen-tickets-1.csv (2 rows) report: out/lumen-tickets.report.jsonsessionId,userId,incoming,text,dimensionlabs_timestamp,conversationId,store,priority,channel4471,c-88,true,My card was charged twice for one flat white.,1767460320000,4471,Portland Pearl,high,email4471,c-88,false,Refund is on its way.,1767460800000,4471,Portland Pearl,high,emailThe email and phone columns were dropped, store_name was renamed, the automatic “Ticket closed” note was skipped, channel was added to every row, and the agent’s reply carries the customer’s id, c-88.
Mapping reference
Section titled “Mapping reference”| Key | What it does |
|---|---|
input.format | auto (default, from the extension), csv, tsv, jsonl, json or xlsx. |
input.sheet | XLSX: the sheet name. Default: the first sheet. |
input.headerRow | The row that holds the column names, counting from 1. Default 1. |
input.skipRowsAfterHeader | Rows to skip under the header, such as a row of question wording. Default 0. |
input.delimiter | Force a delimiter. Default: detected (tab for .tsv). |
input.recordsPath | JSON: the key that holds the array of records, if the file isn’t an array. Default data. |
shape | messages (one row per message, default), transcript (one row per conversation, transcript in a cell) or survey (one row per respondent). |
columns.text, .userId, .sessionId, .conversationId, .timestamp | Source column names. Dot paths such as sender.id read nested JSON. conversationId defaults to the sessionId. Never map an email address, phone number or name to userId or sessionId: use the tool’s internal ids. |
columns.intent, .intentConfidence | Only for an intent-based chatbot: the intent name and its confidence. JSONL output writes them as "intent": { "name": "...", "confidence": 0.94 }. |
userIdForReplies | customer (default): every outgoing message in a session takes the userId of the session’s first incoming message (or the sessionId if there is none), so the bot’s and agent’s replies carry the end user’s id. own keeps each row’s own userId. |
incoming | { "column": "...", "true": [...], "false": [...] }, matched case-insensitively. With only one list, every other value is the other direction. A value in neither list drops the row. Or true / false for the whole file. |
timestamp.format | auto (default), ms, s or excel. auto reads 13-digit milliseconds, 10-digit seconds, numbers below 1,000,000 as Excel serial dates, and date text. Values in scientific notation are dropped, because their digits are lost. |
timestamp.timeZone | The zone of clock times without one, such as America/Chicago. Default UTC, with a note in the report. |
timestamp.dayFirst | true when 03/01/2026 means 3 January. Default false. |
clean.flattenNewlines | Replace line breaks in text with spaces. Default true. |
clean.stripHtml | Remove HTML tags and decode entities. Default false. |
clean.replaceColonsWith | Replacement for : in sessionId and conversationId. Default -. |
metadata.keep | unmapped (default: every column not used above), none, or a list of columns. |
metadata.drop | Columns never to keep, such as emails and phone numbers. |
metadata.rename | { "source name": "new name" }. |
constants | Columns added to every row, such as { "channel": "email" }. |
skip | [{ "column": "...", "values": [...] }]: rows to drop and count, such as system notes. |
dedupe | true removes repeated messages (same session, direction, time and text). Default false. |
spaceMessagesMs | Minimum gap between messages in a session; later messages are moved. Default 1000. 0 turns it off. |
transcript.column, .speakerPattern, .incomingSpeakers, .outgoingSpeakers, .secondsApart, .userId, .stripQuotes | For shape: transcript. The pattern’s first group is the speaker label. Lines without a label join the turn above. userId: "speaker" gives each speaker their own id; the default keeps the row’s userId (or the session id) on every turn. |
survey.questions | For shape: survey: a list of question columns, as names (["why_score", "improve"]) or as { "column": "...", "text": "the question as asked" }. Each answered question becomes an outgoing question row and an incoming answer row. Blank answers are skipped. |
survey.questionsFromRow | The file row, counting from 1, that holds the question wording, such as 2 when the export puts it under the column names. It must be one of the rows skipped by input.skipRowsAfterHeader. A question’s own text wins over it; with neither, the column name is used and the report says so. |
output.format | csv (default) or jsonl (one universal API message per line, metadata nested in platformJson). Both upload. |
output.path | Output prefix. Default: next to the input. |
output.chunkRows | Split output into parts of about this many rows, never cutting a session. Default 0 (one file). |
Tested examples
Section titled “Tested examples”Each example below was run exactly as shown.
Value lists, HTML, colons, seconds, duplicates and empty rows
Section titled “Value lists, HTML, colons, seconds, duplicates and empty rows”ticket_id,author,author_type,body,created_at,store,customer_emailLC:1001,cust-17,Customer,"<p>My oat latte was cold & late.</p>",2026-01-03T17:12:00Z,Portland Pearl,[email protected]LC:1001,agent-4,Agent,"Sorry about that!LC:1003,cust-22,Visitor,Is the Pearl store open on Sunday?,2026-01-04T10:00:00Z,Portland Pearl,[email protected]{ "input": { "format": "auto" }, "shape": "messages", "columns": { "text": "body", "userId": "author", "sessionId": "ticket_id", "timestamp": "created_at" }, "incoming": { "column": "author_type", "true": ["customer"], "false": ["agent", "bot"] }, "timestamp": { "format": "auto", "timeZone": "UTC" }, "clean": { "stripHtml": true, "flattenNewlines": true, "replaceColonsWith": "-" }, "metadata": { "keep": "unmapped", "drop": ["customer_email"] }, "constants": { "channel": "email" }, "dedupe": true, "spaceMessagesMs": 1000, "output": { "format": "csv", "chunkRows": 0 }}node to-dimensionlabs.mjs --config tickets.json --input tickets.csv --out out/ticketsRows in: 7 Messages out: 4 Sessions: 2 Dropped: 3 dropped 1: empty text dropped 1: incoming value not recognized: "visitor" dropped 1: duplicate message fixed 3: ids with colons rewritten fixed 1: html removed from text fixed 1: line breaks in text replaced with spaces fixed 1: timestamps in seconds converted to milliseconds fixed 1: blank userId filled from sessionId fixed 2: replies given the customer's userId fixed 1: messages moved to be 1000 ms apart wrote out/tickets-1.csv (4 rows) report: out/tickets.report.jsonsessionId,userId,incoming,text,dimensionlabs_timestamp,conversationId,store,channelLC-1001,cust-17,true,My oat latte was cold & late.,1767460320000,LC-1001,Portland Pearl,emailLC-1001,cust-17,false,Sorry about that! We've refunded you.,1767460321000,LC-1001,Portland Pearl,emailLC-1002,LC-1002,true,Do you sell decaf beans?,1767460320000,LC-1002,Austin Eastside,emailLC-1002,LC-1002,false,Yes: our Night Owl decaf.,1767460380000,LC-1002,Austin Eastside,emailThe agent’s and the bot’s replies carry the customer’s id: cust-17 in LC-1001, and in LC-1002, where the customer’s id was blank, the session id.
Clock times in a named time zone, TSV in, JSONL out
Section titled “Clock times in a named time zone, TSV in, JSONL out”conv sender who message sentc1 u1 customer Hi, is the Pearl store open? 1/3/2026 9:12 AMc1 bot bot Yes, until 6 PM. 1/3/2026 9:12 AMc2 u2 customer Summer hours? 7/4/2026 12:30 PMc3 u3 customer Loyalty card lost Oct 3 2025 9:12AM{ "columns": { "text": "message", "userId": "sender", "sessionId": "conv", "timestamp": "sent" }, "incoming": { "column": "who", "true": ["customer"] }, "timestamp": { "timeZone": "America/Los_Angeles" }, "output": { "format": "jsonl" }}node to-dimensionlabs.mjs --config chat.json --input chat.tsv --out out/chatRows in: 4 Messages out: 4 Sessions: 3 Dropped: 0 fixed 1: replies given the customer's userId fixed 1: messages moved to be 1000 ms apart wrote out/chat-1.jsonl (4 rows) report: out/chat.report.json{"text":"Loyalty card lost","userId":"u3","incoming":true,"sessionId":"c3","conversationId":"c3","dimensionlabs_timestamp":1759507920000}{"text":"Hi, is the Pearl store open?","userId":"u1","incoming":true,"sessionId":"c1","conversationId":"c1","dimensionlabs_timestamp":1767460320000}{"text":"Yes, until 6 PM.","userId":"u1","incoming":false,"sessionId":"c1","conversationId":"c1","dimensionlabs_timestamp":1767460321000}{"text":"Summer hours?","userId":"u2","incoming":true,"sessionId":"c2","conversationId":"c2","dimensionlabs_timestamp":1783193400000}The bot’s reply carries the customer’s id, u1, not bot. 1/3/2026 9:12 AM in Los Angeles is 1767460320000 (17:12 UTC, standard time); 7/4/2026 12:30 PM is 1783193400000 (19:30 UTC, daylight saving time).
Excel: a named sheet, a title row, serial dates, long ids and an error cell
Section titled “Excel: a named sheet, a title row, serial dates, long ids and an error cell”The workbook chats.xlsx has a Read me sheet and a Chats sheet with a title in row 1. Build it with:
import XLSX from 'xlsx';import * as fs from 'node:fs';XLSX.set_fs(fs);const rows = [ ['Lumen Coffee chat export'], ['Chat ID', 'Visitor', 'Role', 'Message', 'Sent at', 'Order no'], [880011223344, 'v-1', 'Visitor', 'Can I change my pickup store?', 46025.38333333333, 4400123456789], [880011223344, '', 'Barista', 'Sure, which store?', 46025.38333333333, 4400123456789], [880011223345, 'v-2', 'Visitor', 'Gift card balance?', 46026.5, { t: 'e', v: 0x2A }],];const ws = XLSX.utils.aoa_to_sheet(rows);// mark the date column as a date format so it looks like a real exportfor (const r of [3, 4, 5]) { const c = ws['E' + r]; c.z = 'm/d/yyyy h:mm'; }ws['F5'] = { t: 'e', v: 0x2A, w: '#N/A' };const wb = XLSX.utils.book_new();XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet([['notes']]), 'Read me');XLSX.utils.book_append_sheet(wb, ws, 'Chats');XLSX.writeFile(wb, 'chats.xlsx');console.log('ok');{ "input": { "sheet": "Chats", "headerRow": 2 }, "columns": { "text": "Message", "userId": "Visitor", "sessionId": "Chat ID", "timestamp": "Sent at" }, "incoming": { "column": "Role", "true": ["visitor"], "false": ["barista"] }, "timestamp": { "format": "auto", "timeZone": "America/Chicago" }, "metadata": { "rename": { "Order no": "order_number" } }}node make-xlsx.mjsnode to-dimensionlabs.mjs --config xlsx.json --input chats.xlsx --out out/xlsxRows in: 3 Messages out: 3 Sessions: 2 Dropped: 0 fixed 3: excel serial dates converted fixed 1: blank userId filled from sessionId fixed 1: replies given the customer's userId fixed 1: messages moved to be 1000 ms apart note: read sheet "Chats" wrote out/xlsx-1.csv (3 rows) report: out/xlsx.report.jsonsessionId,userId,incoming,text,dimensionlabs_timestamp,conversationId,order_number880011223344,v-1,true,Can I change my pickup store?,1767453120000,880011223344,4400123456789880011223344,v-1,false,"Sure, which store?",1767453121000,880011223344,4400123456789880011223345,v-2,true,Gift card balance?,1767549600000,880011223345,The order number in the last row was a #N/A cell and is left empty. The barista’s reply had no id and now carries the visitor’s, v-1.
Transcripts
Section titled “Transcripts”call_id,started_at,store,transcriptcall-501,2026-02-10T15:00:00Z,Austin Eastside,"Agent: Thanks for calling Lumen Coffee, this is Sam.Customer: Hi, my mobile order never showed up.It was a cold brew and a scone.Agent: I'm sorry. Let me look that up.Customer: Thanks."call-502,2026-02-11T16:30:00Z,Portland Pearl,"Customer: Do you roast on site?Agent: We do, every Tuesday."{ "shape": "transcript", "columns": { "sessionId": "call_id", "timestamp": "started_at" }, "transcript": { "column": "transcript", "speakerPattern": "^(Agent|Customer):\\s*", "incomingSpeakers": ["Customer"], "secondsApart": 5 }, "constants": { "channel": "phone" }}node to-dimensionlabs.mjs --config calls.json --input calls.csv --out out/callsTranscript turns all carry the row’s userId, or the session id when there is none, so both sides already share one id.
sessionId,userId,incoming,text,dimensionlabs_timestamp,conversationId,store,channel,speakercall-501,call-501,false,"Thanks for calling Lumen Coffee, this is Sam.",1770735600000,call-501,Austin Eastside,phone,Agentcall-501,call-501,true,"Hi, my mobile order never showed up. It was a cold brew and a scone.",1770735605000,call-501,Austin Eastside,phone,Customercall-501,call-501,false,I'm sorry. Let me look that up.,1770735610000,call-501,Austin Eastside,phone,Agentcall-501,call-501,true,Thanks.,1770735615000,call-501,Austin Eastside,phone,Customercall-502,call-502,true,Do you roast on site?,1770827400000,call-502,Portland Pearl,phone,Customercall-502,call-502,false,"We do, every Tuesday.",1770827405000,call-502,Portland Pearl,phone,AgentA wide survey with a row of question wording
Section titled “A wide survey with a row of question wording”The export puts the question wording in row 2, under the column names. questionsFromRow takes the question text from that row, and skipRowsAfterHeader keeps it from being read as a respondent.
response_id,submitted_at,nps,why_score,improveResponse ID,Submitted,How likely are you to recommend Lumen Coffee?,Why did you give that score?,What one thing should we improve?R-1,2026-03-01 08:15,9,Friendly baristas.,Faster mobile pickup.R-2,2026-03-01 09:40,4,,More oat milk options.R-3,2026-03-02 18:05,7,,{ "input": { "skipRowsAfterHeader": 1 }, "shape": "survey", "columns": { "sessionId": "response_id", "timestamp": "submitted_at" }, "timestamp": { "timeZone": "America/New_York" }, "survey": { "questionsFromRow": 2, "questions": ["why_score", "improve"] }}node to-dimensionlabs.mjs --config survey.json --input survey.csv --out out/surveyRows in: 3 Messages out: 6 Sessions: 2 Dropped: 3 dropped 3: blank survey answer (question and answer skipped) fixed 6: blank userId filled from sessionId note: question wording read from row 2 note: some respondents answered none of the listed questions and produce no rows wrote out/survey-1.csv (6 rows) report: out/survey.report.jsonsessionId,userId,incoming,text,dimensionlabs_timestamp,conversationId,nps,questionR-1,R-1,false,Why did you give that score?,1772370900000,R-1,9,why_scoreR-1,R-1,true,Friendly baristas.,1772370901000,R-1,9,why_scoreR-1,R-1,false,What one thing should we improve?,1772370902000,R-1,9,improveR-1,R-1,true,Faster mobile pickup.,1772370903000,R-1,9,improveR-2,R-2,false,What one thing should we improve?,1772376000000,R-2,4,improveR-2,R-2,true,More oat milk options.,1772376001000,R-2,4,improveA Qualtrics CSV has two rows under the column names: the question wording, then a row of {"ImportId":...} values. Use "skipRowsAfterHeader": 2 and "questionsFromRow": 2.
Nested JSONL and split output
Section titled “Nested JSONL and split output”{"ticket":{"id":"T-1"},"sender":{"type":"customer","id":"u-301"},"body":"Where is my bag of Night Owl?","created":"2026-04-01T10:00:00Z","tags":["shipping"]}{"ticket":{"id":"T-1"},"sender":{"type":"agent","id":"a-12"},"body":"It ships today.","created":"2026-04-01T10:05:00Z","tags":["shipping"]}{"ticket":{"id":"T-2"},"sender":{"type":"customer","id":"u-302"},"body":"Cancel my subscription","created":"2026-04-02T11:00:00Z","tags":[]}{"ticket":{"id":"T-3"},"sender":{"type":"customer","id":"u-303"},"body":"Thanks!","created":"2026-04-03T11:00:00Z","tags":[]}{"ticket":{"id":"T-3"},"sender":{"type":"agent","id":"a-12"},"body":"Anytime.","created":"2026-04-03T11:01:00Z","tags":[]}not json{ "columns": { "text": "body", "userId": "sender.id", "sessionId": "ticket.id", "timestamp": "created" }, "incoming": { "column": "sender.type", "true": ["customer"], "false": ["agent"] }, "metadata": { "keep": ["tags"] }, "output": { "format": "csv", "chunkRows": 2 }}node to-dimensionlabs.mjs --config jsonl.json --input messages.jsonl --out out/nestedRows in: 6 Messages out: 5 Sessions: 3 Dropped: 1 dropped 1: line 6 is not valid JSON fixed 2: replies given the customer's userId wrote out/nested-1.csv (2 rows) wrote out/nested-2.csv (1 row) wrote out/nested-3.csv (2 rows) report: out/nested.report.jsonWith chunkRows set to 2, ticket T-1 (2 rows), T-2 (1 row) and T-3 (2 rows) each land in their own part: no part can take the next ticket without going over 2 rows.
Read the report
Section titled “Read the report”Every run writes <out>.report.json:
{ "input": "lumen-tickets.csv", "rowsIn": 3, "messagesOut": 2, "sessions": 1, "dropped": { "skipped: author_type is system": 1 }, "fixed": { "html removed from text": 1, "line breaks in text replaced with spaces": 1, "replies given the customer's userId": 1 }, "notes": [], "files": [ { "file": "out/lumen-tickets-1.csv", "rows": 2 } ], "firstTimestamp": "2026-01-03T17:12:00.000Z", "lastTimestamp": "2026-01-03T17:20:00.000Z"}rowsInandmessagesOutshould make sense together. Formessages, out is in minus dropped. Transcripts and surveys write several rows per input row.droppedlists every reason a row (or a survey answer) was left out. An unexpectedincoming value not recognizedmeans a value is missing from your lists.fixedcounts each fix. Large numbers are fine when you expect them (seconds converted to milliseconds on every row of a file that uses seconds); a surprise is worth a look.notesflags things to check, such as times read as UTC because no time zone was set, or kept columns that look like personal data (add those tometadata.drop; see PII redaction).firstTimestampandlastTimestampshould match the date range of your export.
Then run the file checker on the output and go through Before you send it.
Monthly exports
Section titled “Monthly exports”Keep one mapping file per export type next to your data, under a name such as helpdesk-tickets.mapping.json. Next month, run the same command on the new file and upload it to the same source. If the new export overlaps last month’s, that’s fine: Dimension Labs removes duplicate messages when it ingests them. If the export’s columns change, the run tells you: a missing column shows up as dropped rows (no sessionId, empty text) instead of quietly shifted data.
What it doesn’t do
Section titled “What it doesn’t do”- Cut quoted replies from emails: see Clean ticket and email text.
- Shift dates by a known error, such as a wrong year: see Fix timestamps.
- Join business fields from a second file: see Attach business fields.
- Combine several exports: run it once per export, then see Combine several exports.
- Name speakers’ sides in call transcripts that use people’s names: see Turn call transcripts into turns.