# CLI

URL: https://better-helpdesk.com/docs/operations/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](/docs/operations/switching).

## Environment [#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 [#better-helpdesk-migrate]

```sh
HELPDESK_DATABASE_URL=postgres://user:password@localhost:5432/app npx better-helpdesk-migrate
```

Applies 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 [#better-helpdesk-import]

```sh
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 [#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 [#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 [#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 [#conversations]

One JSON object per line, in the format `better-helpdesk-export` writes; see [the export](#better-helpdesk-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 [#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:

```txt
helpdesk: 12 created, 3 updated, 1 unchanged or skipped
```

## better-helpdesk-export [#better-helpdesk-export]

```sh
HELPDESK_DATABASE_URL=postgres://… npx better-helpdesk-export --prefix ACME > conversations.jsonl
npx better-helpdesk-export --prefix ACME --since 2026-01-01 > this-year.jsonl
```

Writes 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`.
