CLI
The three commands the package installs, better-helpdesk-migrate, better-helpdesk-import and better-helpdesk-export, with their arguments, environment and output.
The package installs three commands, run with npx from the host app. Each opens one connection to the database the URL names, through the driver the host installed, and closes it on exit. Related guide: Switch from another helpdesk.
Environment
All three commands read the same variables.
| Variable | Meaning |
|---|---|
HELPDESK_DATABASE_URL | Connection string. Read first. Its scheme picks the database: postgres:// or postgresql:// (through pg), mysql:// (mysql2, in UTC), mssql:// or sqlserver:// (tedious and tarn; ?encrypt=false and ?trustServerCertificate=true are passed on), and file: or a path ending in .db, .sqlite or .sqlite3 (Node's own node:sqlite). |
APP_DATABASE_URL | Used when HELPDESK_DATABASE_URL is unset. |
DATABASE_SSL | Postgres only. true turns on TLS and verifies the server's certificate. no-verify turns on TLS without verifying it, for a self-signed certificate. A sslmode in the connection string takes precedence over both. |
Without a connection string, the command prints HELPDESK_DATABASE_URL or APP_DATABASE_URL must be set and exits with status 1. With a scheme it does not know, or without the driver the scheme needs, it says so and exits with status 1.
better-helpdesk-migrate
HELPDESK_DATABASE_URL=postgres://user:password@localhost:5432/app npx better-helpdesk-migrateApplies the package's migrations: it creates the helpdesk schema and its tables on the first run and brings them up to date on later ones. Applied migrations are recorded in helpdesk.migration; an install that drizzle migrated before keeps its history in helpdesk.__migrations, and only what it lacks runs. A lock keeps two runs from applying the same migration. It takes no arguments and prints the migrations it applied, or helpdesk: up to date.
better-helpdesk-import
npx better-helpdesk-import contacts <file.csv> [--lead-stages a,b,c]
npx better-helpdesk-import canned <file.csv>
npx better-helpdesk-import conversations <file.jsonl> [--source Freshdesk]Imports contacts and their companies, or canned replies, from a CSV file, or conversation history from a JSON Lines file. The file is read before the connection opens, so a file that cannot be read writes nothing. Running a CSV import again adds what is new and changes nothing an agent edited. A conversation import opens every line as a new conversation, so run it once.
| Argument | Meaning |
|---|---|
contacts, canned or conversations | What the file holds. |
<file> | Path to the file. |
--lead-stages a,b,c | The host's leadStages, for contacts. Unset, the package's defaults: lead, qualified, customer, churned. |
--source Freshdesk | For conversations: where the history came from, kept with each conversation. |
Any other arguments print the usage line and exit with status 1.
CSV format
RFC 4180: comma-separated, fields in double quotes when they hold commas, quotes or line breaks, a quote inside a quoted field doubled. The first row is the header; its names are trimmed and lower-cased, and values are trimmed. Blank rows are skipped. A leading byte order mark is ignored. A quote that never closes, or text after a closing quote, stops the import with the line number and exit status 1.
contacts
| Column | Required | Meaning |
|---|---|---|
email | yes | Matches an existing contact, case-insensitively. A row without a usable address is skipped. |
name | Up to 200 characters. | |
company | Company name, up to 200 characters. | |
domain | Company domain, up to 200 characters. | |
tags | Separated by ; or |; lower-cased, each up to 50 characters, at most 50 per contact. | |
lead_stage | One of the lead stages, matched case-insensitively. Others are left out. |
The company is found by domain when the row has one, else by company name, and created when missing, named after the domain when the row has no company; an existing company is never renamed. A company found by name without a domain takes the row's domain.
A new contact is created with the row's values. An existing contact only gains what it lacks: empty fields are filled and new tags are added.
canned
| Column | Required | Meaning |
|---|---|---|
title | yes | Up to 200 characters. |
body | yes | Up to 20,000 characters. |
locale | en, de, fr or it. Anything else saves the reply for any language. |
A row without a title or a body, or over either limit, is skipped. A reply whose title and locale match an existing one is left alone.
conversations
One JSON object per line, in the format better-helpdesk-export writes; see the export. A line needs inbox, type, createdAt, a contact and at least one message. A line without a priority gets normal, so give each line one of your own when your priorities differ. Each line opens a new conversation with a new reference; the old reference and --source are kept in the conversation's context as imported, so agents can still quote it. Contacts are found by email, or made; a company by domain, then name, or made; agents by email, and a message from an agent nobody here has is kept as an agent message without a name. Attachments and timeline events are not imported, and no deadline starts. A line that is not JSON, or fails validation, is skipped with a note naming the line.
Output
Each value left out prints a note to stderr, such as helpdesk: row 4: no usable email, skipped, where the row number counts the header as row 1 and a JSON Lines file counts from line 1. The last line counts the rows:
helpdesk: 12 created, 3 updated, 1 unchanged or skippedbetter-helpdesk-export
HELPDESK_DATABASE_URL=postgres://… npx better-helpdesk-export --prefix ACME > conversations.jsonl
npx better-helpdesk-export --prefix ACME --since 2026-01-01 > this-year.jsonlWrites every conversation to stdout as one JSON object per line, oldest id first, and counts them on stderr. --prefix is your referencePrefix, so the references match what customers were told; HD when unset. --since keeps the conversations opened on or after that day.
Each line holds the conversation's reference, inbox, type, kind, channel, status, priority, tier, subject, title, tags, context, createdAt, resolvedAt, rating, ratingScore and ratingComment; its contact and company by name, email and domain; its participants; its messages with author (contact, agent or system), the author's name and email, body, internal, createdAt and emailMessageId; its attachments by filename, type, size, storage key and the index of their message; and its timeline events. People are named by email rather than id, so the file reads into another install, and a script that maps another tool's export to these fields moves its history in through better-helpdesk-import conversations.