Export CRM Notes to CSV Without Breaking Commas or Line Breaks
How can I export CRM notes from Google Sheets to a UTF-8 CSV file without breaking commas, quotes, or line breaks?
Use Google Sheets with a bound Google Apps Script to export displayed values as fully quoted CSV, double embedded quotation marks, and separate records with CRLF. Save the CSV as UTF-8 in Drive, preserving internal note line breaks, and import it with a parser that supports quoted multiline fields.
What you need
- A Google Sheets spreadsheet with a populated source tab
- An Apps Script project bound to the source spreadsheet
- Google authorization for spreadsheet and Drive access
- EXPORT_FOLDER_ID set to a Drive folder where the executing account can create files
How this script runs
- You run the exporter
- It reads the displayed values from the sheet
- It quotes every field and doubles embedded quotes
- It joins records with CRLF line endings
- It saves a UTF-8 CSV file to Drive
How does the Google Sheets exporter preserve CRM note boundaries?
The Google Sheets Apps Script exporter quotes every field, doubles embedded quotation marks, and separates records with CRLF while preserving line breaks inside notes.
Quoting keeps commas inside their fields. Doubling quotes prevents quoted wording from ending a field early. CRLF, written as \r\n in JavaScript, separates records without flattening multiline notes.
The script exports displayed values, including formatted dates and visible leading zeros, rather than formulas or spreadsheet formatting. It saves UTF-8 text without a byte order mark, or BOM.
What sample CRM data should the source tab contain?
A tab named CRM Notes should contain the following sample data starting at A1 to illustrate commas, embedded quotes, and multiline notes.
| Lead ID | Contact | Notes |
|---|---|---|
| CRM-001 | Pixel Placeholder | Call Tuesday, after 2 PM. |
| CRM-002 | Nova Example | Said “email only”. |
| CRM-003 | Orbit Demo | Requested recap.[LF]Send Monday. |
These contacts are fictional placeholders. Replace [LF] with one actual in-cell line break, placing Send Monday. on the second line.
The export rectangle extends from A1 through the last content-bearing row and column. Keep unrelated content off this tab. Hidden and filtered-out rows remain included; blank cells become "", and blank rows between populated rows remain.
What Apps Script code creates the quoted UTF-8 CSV?
The following bound Google Apps Script exports displayed values from the configured tab into a new timestamped CSV file in a specified Drive folder.
function exportCrmNotesCsv() {
const properties = PropertiesService.getScriptProperties();
const sheetName =
properties.getProperty('SOURCE_SHEET_NAME') || 'CRM Notes';
const folderId = properties.getProperty('EXPORT_FOLDER_ID');
if (!folderId || !folderId.trim()) {
throw new Error(
'Set EXPORT_FOLDER_ID in Script Properties to a Drive folder ID.'
);
}
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
if (!spreadsheet) {
throw new Error(
'Run this script from an Apps Script project bound to a spreadsheet.'
);
}
const sheet = spreadsheet.getSheetByName(sheetName);
if (!sheet) {
throw new Error('Sheet not found: ' + sheetName);
}
const lastRow = sheet.getLastRow();
const lastColumn = sheet.getLastColumn();
if (lastRow === 0 || lastColumn === 0) {
throw new Error('The source sheet is empty: ' + sheetName);
}
const values = sheet
.getRange(1, 1, lastRow, lastColumn)
.getDisplayValues();
const csv =
values
.map(function (row) {
return row.map(quoteCsvField).join(',');
})
.join('\r\n') + '\r\n';
const timestamp = Utilities.formatDate(
new Date(),
'Etc/UTC',
"yyyyMMdd'T'HHmmssSSS'Z'"
);
const fileName = 'crm-notes-' + timestamp + '.csv';
const blob = Utilities.newBlob('')
.setDataFromString(csv, 'UTF-8')
.setContentType('text/csv')
.setName(fileName);
const folder = DriveApp.getFolderById(folderId.trim());
const file = folder.createFile(blob);
Logger.log('CSV saved: ' + file.getUrl());
return file.getUrl();
}
function quoteCsvField(value) {
return '"' + String(value).replace(/"/g, '""') + '"';
}
quoteCsvField doubles existing quotes before wrapping the value. Backslash escapes such as \" are not CSV escapes. The final + '\r\n' terminates the last record rather than adding another data record for a conforming parser.
No API key is required. Google authorization supplies spreadsheet and Drive access; configuration belongs in Script Properties.
How do you configure the source tab and Drive destination?
Configure this CSV exporter by binding the script to the source spreadsheet and setting the destination folder ID in Script Properties.
- Open the source spreadsheet. Keep only intended export columns on the source tab, with headers in row 1.
- Create the bound project. Choose Extensions, then Apps Script. Replace the starter code with the complete script above and save.
- Choose a Drive folder. Select a folder where the executing account can create files. Review its sharing settings before exporting sensitive notes.
- Copy the folder ID. Copy the portion following
/folders/in the folder’s browser address, not its name or complete address. - Set Script Properties. In Apps Script Project Settings, add
EXPORT_FOLDER_ID. AddSOURCE_SHEET_NAMEwith the exact tab name, including capitalization and spaces; omitting it defaults toCRM Notes. - Authorize and export. Select
exportCrmNotesCsv, click Run, and review the requested permissions. Authorize the intended account; rerun if authorization interrupts execution. - Locate the CSV. Find the saved file’s Drive address in the execution log. Download the file for importers requiring local uploads.
Each invocation creates a new file, so repeated exports accumulate. The script neither changes sharing permissions nor sends files to another service.
What CSV output should the sample CRM notes produce?
The sample CRM Notes table produces four CSV records including the header, with three fields per record and one internal line break in the final note.
"Lead ID","Contact","Notes"
"CRM-001","Pixel Placeholder","Call Tuesday, after 2 PM."
"CRM-002","Nova Example","Said ""email only""."
"CRM-003","Orbit Demo","Requested recap.
Send Monday."
Markdown cannot reliably distinguish LF from CRLF. The saved file ends every record with CRLF, including the last; the note retains its internal LF.
The final data record has this escaped representation:
"CRM-003","Orbit Demo","Requested recap.\nSend Monday."\r\n
Here, \n and \r\n describe control characters, not literal backslashes. Counting physical text lines does not count CSV records correctly.
For Excel, choose Data, then From Text/CSV, select UTF-8 with a comma delimiter, and choose Transform Data. If a multiline note splits into separate rows, open the Source step under Applied Steps and set the line break handling to ignore quoted line breaks, which appears as QuoteStyle.Csv in the formula bar. Set identifier columns to Text when automatic conversion could remove leading zeros. CSV quoting protects structure, not data types.
Why does exportCrmNotesCsv preserve multiline CRM notes in CSV?
exportCrmNotesCsv preserves multiline CRM notes by quoting every field, doubling embedded quotes, and inserting record separators without changing note contents.
First, getScriptProperties() reads configuration separately from the code, defaulting the source tab to CRM Notes. getActiveSpreadsheet() and getSheetByName() locate that tab. getLastRow() and getLastColumn() define the export rectangle instead of selecting the entire sheet grid. getDisplayValues() then retrieves visible text rather than raw typed values, retaining displayed dates and leading zeros.
The nested map() calls process rows and fields. For each field, quoteCsvField() converts the value to text, doubles every embedded quotation mark with replace(), and adds surrounding quotes. Applying this uniformly avoids conditional quoting rules while leaving internal line breaks untouched. The inner join(',') separates fields; the outer join('\r\n') separates records, and the final addition terminates the last record.
Next, formatDate() produces a UTC timestamp for the filename. newBlob() and setDataFromString() encode the CSV explicitly as UTF-8; setContentType() and setName() label it. Finally, getFolderById() selects the configured destination, createFile() saves a new file rather than overwriting an existing export, and the script logs and returns its URL.
How do you fix missing folders, missing tabs, and split multiline notes?
Fix exporter failures by correcting Script Properties or folder access, and fix split multiline notes by using an importer that supports quoted multiline CSV fields.
Missing destination folder
Symptom: Set EXPORT_FOLDER_ID in Script Properties to a Drive folder ID.
Exact fix: Add EXPORT_FOLDER_ID in this project’s Project Settings. Supply the folder ID, not an empty value or full address, save, and rerun exportCrmNotesCsv.
For failures at DriveApp.getFolderById or folder.createFile, verify the ID identifies a folder. Grant the executing account permission to create files there or select a folder it controls.
Missing source tab
Symptom: Sheet not found: followed by a name.
Exact fix: Set SOURCE_SHEET_NAME to the exact bottom-tab label, not the spreadsheet document title. Remove accidental leading or trailing spaces unless they belong to the tab name.
Missing spreadsheet or empty source
Exact fix: For the bound-project error, open the source spreadsheet’s Extensions, then Apps Script, and install the code there. For The source sheet is empty:, populate the configured tab or select the correct populated tab.
Multiline note becomes another row
Symptom: Send Monday. becomes a separate record, or column counts differ.
Exact fix: Configure the importer for quoted multiline fields, " as the quote character, and doubled embedded quotes. Import the original Drive file, not preview text.
Replace importers that process each physical line independently with a CSV-aware parser. Do not split on newlines or remove quotation marks.
When should you avoid this displayed-value CSV exporter?
Avoid this Google Sheets exporter when exports exceed Apps Script limits, downstream systems require raw typed values, or spreadsheet formula interpretation threatens untrusted notes.
- Large archives: The script builds the entire CSV in memory. Use a native CRM export or streaming job with a CSV library when execution or memory limits are exceeded. Arbitrary chunk appends risk broken boundaries or encoding.
- Machine-readable values: Use a dedicated export tab with explicit formats or field-specific serialization from raw values. Simply replacing
getDisplayValues()withgetValues()introduces values such as date objects that require deliberate conversion. - Untrusted notes: Quoting does not prevent spreadsheet formula injection; a note beginning with
=may become a formula. Use a workflow that explicitly imports notes as text. If exact content must remain unchanged, prefer an API with typed text fields rather than adding prefixes.
Related questions
Does the exporter include hidden or filtered-out rows?
The Google Sheets exporter includes hidden and filtered-out rows within the rectangle from A1 through the last content-bearing row and column. Blank cells and intervening blank rows are included too.
Will leading zeros in contact IDs survive the export?
The script exports displayed values, so leading zeros visible in Google Sheets appear in the CSV. The receiving application can still convert quoted IDs to numbers, so configure identifier columns as Text during import.
Does each export replace the previous Drive file?
Each invocation creates a new timestamped CSV file in the configured Drive folder. The script does not update previous exports or change sharing permissions.
Does quoting CRM notes prevent spreadsheet formula injection?
CSV quoting protects field boundaries, not against formula interpretation by spreadsheet software. For untrusted notes, use an import workflow that explicitly treats notes as text or a destination API with typed text fields.