> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useterse.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# CLI reference

> Reference for the terse CLI

The `terse` CLI has setup commands (`terse install`, `terse update`), one scaffold command (`terse init`), and a shared workflow CLI.

`terse install` is the recommended first step on a new machine. `terse init` scaffolds a new TypeScript project. The rest of the CLI auto-detects a Terse TypeScript project from the current directory and loads workflows from the default entry file unless you pass `--entry-file`.

| Language   | Project markers                 | Default entry file                                                                  | Generated file           |
| ---------- | ------------------------------- | ----------------------------------------------------------------------------------- | ------------------------ |
| TypeScript | `package.json`, `tsconfig.json` | `src/terse.jobs.ts` (falls back to `src/index.ts` when loading an existing project) | `src/terse.generated.ts` |

The CLI scaffolds, generates helpers for, and runs TypeScript workflows on Node.js only.

`terse test` and `terse deploy` discover every workflow registered with `createJob()` in the entry file's module graph, including files reached by side-effect imports. Keep workflow `name` values unique.

## Getting started

### `terse install`

One-command setup for a fresh machine.

```bash theme={null}
npx terse-cli install
```

What it does, in order:

1. Offers to install the Terse CLI globally (`npm install -g terse-cli`) if `terse` is not already on your `PATH`
2. Installs the official Terse agent skills (`terse-create`, `terse-improve`, `terse-self-host`) into every coding agent detected on your machine via the [skills CLI](https://skills.sh)
3. If `~/.claude` exists, optionally (prompted, never silent) adds `Bash(terse:*)` to `permissions.allow` in `~/.claude/settings.json` so Claude Code can run terse commands without prompting
4. Runs `terse auth login` if you are not already logged in

The skill list is fetched from `.claude-plugin/marketplace.json` on main at runtime, so skill renames and additions propagate to older CLIs without a new CLI release. If the manifest is unreachable, the command fails with an error (there is no baked-in fallback list).

Options:

| Flag                    | Description                                                                                                    |
| ----------------------- | -------------------------------------------------------------------------------------------------------------- |
| `-y, --non-interactive` | Skip prompts. Does not edit Claude Code settings or log you in. Implied automatically when stdin is not a TTY. |

### `terse update`

Updates the globally installed CLI and re-syncs the Terse agent skills.

```bash theme={null}
terse update
```

If `terse` is not installed globally, the command skips the CLI self-update and tells you to run `terse install`.

### `terse init [project-name]`

Scaffolds a new TypeScript project and runs the full setup flow: install dependencies, authenticate, create a Terse project, review integrations, and generate helpers.

```bash theme={null}
terse init my-project
```

If you omit `project-name`, the CLI scaffolds into the current directory. If you run `terse init` in an existing npm project, the CLI tells you to use `terse attach` instead.

The flow, in order:

1. Creates the target directory and scaffolds `package.json`, `tsconfig.json`, `src/terse.jobs.ts`, `src/jobs/sample-job.ts`, `.env.example`, and `.gitignore`
2. Detects `pnpm` or `npm` and installs dependencies
3. Runs `terse auth login`
4. Creates a Terse project and writes `terse.config.json`
5. Reviews your current integrations
6. Runs `terse generate`

Options:

| Flag                    | Description                                                                                                         |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `-y, --non-interactive` | Fail fast instead of prompting. Requires a prior `terse auth login`. Implied automatically when stdin is not a TTY. |

### `terse attach`

Links an existing repo to Terse with a self-hosted data plane (the Hybrid deployment).

```bash theme={null}
terse attach
```

Use `terse attach` when you already have an application repo and want the control plane to sync workflows to a data plane you operate instead of uploading source to be run on the Terse Cloud data plane.

What it does:

1. Authenticates with the control plane, prompting you to pick which organization owns this project when your account belongs to more than one
2. Creates a Terse project if the repo is not already linked
3. Prints the `TERSE_PROJECT_KEY` and `TERSE_SIGNING_SECRET` your data plane needs. The two are always issued together, so a project holding neither gets both generated, and a project already holding either one is asked before both are replaced. Save them now; they will not be shown again
4. Offers to regenerate any credential the project already has, since neither value can be shown twice. Declining changes nothing
5. Writes `terse.config.json` with the self-hosted data plane enabled
6. Reviews existing integrations and, in an interactive terminal, lets you connect more
7. Runs `terse generate` when the current directory matches a supported project layout

Re-running `terse attach` on an attached project is safe and repeatable: it never replaces a working credential unless you confirm the prompt or pass `--regenerate-credentials`. Regenerating revokes the current value immediately, so update your data plane before its next trigger.

Options:

| Flag                       | Description                                                                                                                               |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `-y, --non-interactive`    | Fail fast instead of prompting. Requires a prior `terse auth login`. Implied automatically when stdin is not a TTY.                       |
| `--regenerate-credentials` | Replace the project key and signing secret the project already has, without prompting. The only way to regenerate them non-interactively. |

Before you run `terse deploy`, set `remoteServerUrl` in `terse.config.json` to the URL where your Terse SDK server is running.

```json theme={null}
{
    "projectId": "proj_123",
    "name": "my-app",
    "selfHosted": true,
    "remoteServerUrl": "https://your-app.example.com"
}
```

If your app keeps workflow definitions outside the default entry file, use `--entry-file` with `terse test` and `terse deploy`.

### `terse test [job-name]`

Fetches sample events and runs a local workflow against one selected event.

```bash theme={null}
terse test
terse test my-job
```

If you only have one workflow, the CLI selects it automatically. If you have more than one, the CLI prompts you to choose unless you pass `job-name`.

`terse test` is the interactive picker and requires a terminal. In non-interactive contexts (CI, agents, scripts), use the `terse test list` / `terse test show` / `terse test run` subcommands below.

Options:

| Flag                  | Description                               | Default             |
| --------------------- | ----------------------------------------- | ------------------- |
| `-v, --verbose`       | Show agent stream output during the run   | Enabled             |
| `--no-verbose`        | Hide agent stream output                  |                     |
| `--fresh-state`       | Reset the job's test state before running |                     |
| `--entry-file <path>` | Override the default workflow entry file  | `src/terse.jobs.ts` |

Loads your local workflow, lets you pick a real or synthesized sample event, and runs the handler in-process.

If your workflow uses a webhook trigger but no deliveries are stored yet, the CLI prints a `curl` example against your webhook URL instead of opening an empty picker. Run `terse deploy` first so the CLI prints the URL, POST a test payload to it, then run `terse test` again.

#### `terse test list [job-name]`

Lists sample events for a workflow and assigns each one a content-addressed id.

```bash theme={null}
terse test list
terse test list my-job --json
```

Use this command when you want a stable handle for a sample event without opening the interactive picker. The id stays the same as long as the underlying trigger payload stays the same. With `--json`, the output includes `webhookEndpoints` (URLs you can POST to) when your workflow has a webhook trigger, useful for scripting alongside stored samples.

Options:

| Flag                  | Description                                                |
| --------------------- | ---------------------------------------------------------- |
| `--json`              | Emit JSON, including the full serialized event for each id |
| `--entry-file <path>` | Override the default workflow entry file                   |

Sample-event ids are deterministic content hashes, so the same payload keeps the same id across runs.

#### `terse test show <id> [job-name]`

Shows the contents of one sample event.

```bash theme={null}
terse test show 3f4d1c8a9b12
terse test show 3f4d1c8a9b12 my-job --json
```

Options:

| Flag                  | Description                                 |
| --------------------- | ------------------------------------------- |
| `--json`              | Emit JSON instead of the rendered text view |
| `--entry-file <path>` | Override the default workflow entry file    |

The CLI refetches the workflow's current sample events and resolves the id against them. With `--json`, the event is printed as a trigger event fixture: save it to a file, edit it, and replay it with `terse test run --event-file`.

#### `terse test run [job-name]`

Runs a workflow against an explicit sample event.

```bash theme={null}
terse test run --id 3f4d1c8a9b12
terse test run my-job --event-file fixture.json
```

Pass exactly one of `--id`, `--event`, or `--event-file`.

Options:

| Flag                  | Description                                                |
| --------------------- | ---------------------------------------------------------- |
| `--id <id>`           | Run a sample event by its `terse test list` id             |
| `--event <json>`      | Inline trigger event fixture JSON                          |
| `--event-file <path>` | Path to a JSON file containing a trigger event fixture     |
| `-v, --verbose`       | Show agent stream output during the run (default: enabled) |
| `--no-verbose`        | Hide agent stream output                                   |
| `--fresh-state`       | Reset the job's test state before running                  |
| `--entry-file <path>` | Override the default workflow entry file                   |

Test runs read and write an isolated copy of the job's [state](/reference/typescript-sdk#job-state) that persists across `terse test` invocations, so dedupe and cooldown logic behaves the same way it will in production. Pass `--fresh-state` to reset that test state before the run, for example to re-test a `filter` that already fired. `--fresh-state` never touches the state of the deployed job, and is not yet supported for projects with a remote data plane.

##### Trigger event fixtures

`--event` and `--event-file` take a flat trigger event fixture: the same fields your handler reads, and nothing else. For a webhook trigger:

```json theme={null}
{
    "integrationType": "webhook",
    "eventType": "webhook",
    "method": "POST",
    "headers": { "content-type": "application/json" },
    "body": { "lead": "acme" }
}
```

The available fields per trigger type match what your handler receives at runtime (for webhooks: `body`, `headers`, `method`).

The prompt-facing text returned by `event.formatForAgentRunner()` and the log line returned by `event.debugLog()` are derived from the fixture exactly the way production derives them from a live event. To test a custom rendering, add a top-level `formattedContent` or `debugLog` string to the fixture and it overrides the derived value.

`event.triggeredAt` defaults to the time you run the test. Add a top-level `triggeredAt` ISO 8601 timestamp (for example `"2026-07-01T09:00:00.000Z"`) to pin it, so date windows derived from it stay the same on every run.

`terse test show <id> --json` prints any stored sample event in this shape, so a recorded event is always a valid starting point for a hand-edited fixture.

### `terse deploy`

Packages your local project and syncs workflows to Terse. [Learn more about how deployment works.](/hosting)

```bash theme={null}
terse deploy
terse deploy --entry-file src/server.ts
```

Options:

| Flag                  | Description                              |
| --------------------- | ---------------------------------------- |
| `--entry-file <path>` | Override the default workflow entry file |

What happens:

* New workflows are created on the platform
* Existing workflows are updated
* Workflows removed from your code are removed from Terse
* For each workflow that includes a webhook trigger, the CLI prints a Webhook URL line after deploy so you can wire up external systems without opening the app
* For managed projects (Terse Cloud data plane) with a local `.env`, the CLI offers to upload any variables not already stored as project secrets before the deploy runs. Use [`terse secrets`](#terse-secrets) for finer-grained control.
* Durable workflows (`durable: true`) use Terse's journal runtime directly; there is no separate workflow bundle to compile.

Data plane targets:

* Managed (Terse Cloud data plane): the CLI zips the current project directory, uploads it, and the control plane builds a source image before running workflows on Terse Cloud sandboxes
* Self-hosted data plane: the CLI reads `remoteServerUrl` from `terse.config.json` and configures the control plane to call your own server instead of uploading source. `terse attach` is where `TERSE_PROJECT_KEY` and `TERSE_SIGNING_SECRET` are issued and printed. Deploy prints either one only as a fallback, for a project that somehow reached deploy without it.

### `terse build`

Prepares the transformed TypeScript sources used by durable jobs in `.terse/runtime`.

```bash theme={null}
terse build
```

Terse normally prepares these sources automatically when it loads your jobs. Run `terse build` locally only when you want to inspect the inline step transformation or diagnose a source-transform error before deploying.

## Build with workspace context

### `terse integrate`

Interactive integration management from the terminal.

```bash theme={null}
terse integrate
```

The interactive flow fetches your current integrations, shows provider-specific status, lets you connect, disconnect, or refresh one integration at a time, and reruns `terse generate` after any changes.

Connection types:

| Type  | Flow                                                                       | Integrations                                                        |
| ----- | -------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| OAuth | Opens a browser for authorization and waits for the connection to complete | GitHub, Slack, Gmail, Google Search Console, Linear, Notion, Attio  |
| Form  | Prompts for credentials or account details in the terminal                 | Datadog, PostHog, Snowflake, LaunchDarkly, WorkOS, HeyReach, Apollo |

#### `terse integrate list`

Lists integrations and their connection status.

```bash theme={null}
terse integrate list
terse integrate list --status connected --json
```

Options:

| Flag                | Description                             |
| ------------------- | --------------------------------------- |
| `--status <status>` | Filter to `connected` or `disconnected` |
| `--json`            | Emit machine-readable JSON              |

#### `terse integrate describe <type>`

Shows the current status, installation type, required fields, any setup URL, and a summary of the tools the integration exposes to jobs (name, description, read-only flag, approval support).

```bash theme={null}
terse integrate describe snowflake
terse integrate describe slack --json
```

Use this command to inspect the schema before you build a `connect` command. For a tool's input/output schemas, use `terse integrate tool <type> <tool-name>`.

Options:

| Flag     | Description                |
| -------- | -------------------------- |
| `--json` | Emit machine-readable JSON |

#### `terse integrate tool <type> [tool-name]`

With only an integration type, lists every tool that integration exposes to jobs, with descriptions. With a tool name, shows that tool's full details: description, read-only flag, approval support, input schema, and output schema.

```bash theme={null}
terse integrate tool slack
terse integrate tool slack --json
terse integrate tool slack slack_send_message --json
```

Options:

| Flag     | Description                |
| -------- | -------------------------- |
| `--json` | Emit machine-readable JSON |

#### `terse integrate tool run <tool>`

Invokes a read-only toolbox tool ad hoc and prints its JSON result.

```bash theme={null}
terse integrate tool run slack_list_channels
terse integrate tool run attio_records --params '{"request":{"action":"query","objectSlug":"deals","limit":5}}'
echo '{"request":{"action":"search","objectSlug":"companies","query":"acme"}}' | terse integrate tool run attio_records
terse integrate tool run linear_search_ticket --integration cm123 --params '{"query":"TER-658"}'
```

Options:

| Flag                 | Description                                                            |
| -------------------- | ---------------------------------------------------------------------- |
| `--params <json>`    | Tool params as a JSON object; pass `-` (or pipe) to read from stdin    |
| `--integration <id>` | Integration connection ID (only needed when several connections exist) |

#### `terse integrate connections <type>`

Lists an integration's connections with their IDs and which one this project pins.

```bash theme={null}
terse integrate connections slack
terse integrate connections slack --json
```

With `--json`, the output is a `{ id, name, pinned }` object per connection.

Options:

| Flag     | Description                |
| -------- | -------------------------- |
| `--json` | Emit machine-readable JSON |

#### `terse integrate use <type> [connection-id]`

Pins which connection this project generates against, then reruns `terse generate` so `src/terse.generated.ts` always matches the pin. Without a pin, generation uses the integration's oldest connection.

```bash theme={null}
terse integrate use slack                            # interactive picker (auto-pins a single connection)
terse integrate use slack cmr3l8b3d0003bpq4rpifevze  # explicit id, for agents and scripts
terse integrate use slack --clear                    # remove the pin and regenerate
```

The pin is written to the `connections` map in `terse.config.json` and committed with the project, so every machine and CI generates against the same connection. `terse integrate` and `terse integrate list` show a pinned connection with a `(pinned)` marker; a pin whose connection no longer exists shows `(pinned, missing)` and fails `terse generate` with a re-pin hint.

In non-interactive terminals, omitting `connection-id` fails unless the integration has exactly one connection, which is auto-pinned. Use `terse integrate connections <type>` to find the id first.

Options:

| Flag      | Description                                  |
| --------- | -------------------------------------------- |
| `--clear` | Remove this integration's pin and regenerate |

#### `terse integrate connect <type>`

Connects or refreshes one integration without using the interactive picker.

```bash theme={null}
terse integrate connect snowflake --field account=my-account --field username=alice --fields-stdin <<<'{"password":"..."}'
terse integrate connect slack
```

Options:

| Flag                  | Description                                                     |
| --------------------- | --------------------------------------------------------------- |
| `--field <key=value>` | Repeatable form field values                                    |
| `--fields-stdin`      | Read a JSON object of additional field values from stdin        |
| `-f, --force`         | Re-run the install even if the integration is already connected |
| `--json`              | Emit machine-readable JSON                                      |

Use `--fields-stdin` for secrets so passwords and tokens do not end up in shell history.

For form-based integrations, `connect` submits the provided field values immediately. For OAuth integrations, `connect` opens the authorization URL in your default browser and exits 2 with a handoff payload (`{ "handoff": "oauth", "url", "waitCommand" }` in `--json` mode, or an `ACTION REQUIRED` line otherwise). Run the printed `waitCommand` (e.g. `terse integrate wait slack`) to block until authorization completes.

#### `terse integrate disconnect <type>`

Disconnects one integration.

```bash theme={null}
terse integrate disconnect snowflake
terse integrate disconnect slack --json
```

Options:

| Flag     | Description                |
| -------- | -------------------------- |
| `--json` | Emit machine-readable JSON |

#### `terse integrate wait <type>`

Polls until an OAuth integration finishes connecting.

```bash theme={null}
terse integrate connect slack
terse integrate wait slack --timeout 300
```

Use this after `terse integrate connect <type>` for OAuth integrations such as Slack or GitHub.

Options:

| Flag                  | Description                                         |
| --------------------- | --------------------------------------------------- |
| `--timeout <seconds>` | Timeout in seconds. Defaults to 300 and caps at 900 |
| `--json`              | Emit machine-readable JSON                          |

The `list`, `describe`, `tool`, `tool run`, `connections`, `connect`, `disconnect`, and `wait` subcommands do not rerun code generation for you. After a connection change, run `terse generate`. `terse integrate use` is the exception: it reruns generation as part of pinning.

### `terse secrets`

Manages project secrets for projects using the Terse Cloud data plane. Secrets are stored on the control plane and injected into Terse Cloud sandbox runs as environment variables. Projects with a self-hosted data plane manage their own runtime env vars and these commands return an error there.

```bash theme={null}
terse secrets list
```

#### `terse secrets list`

Lists secret names (not values) stored on the linked Terse project.

| Flag     | Description |
| -------- | ----------- |
| `--json` | Emit JSON   |

#### `terse secrets add <NAME>`

Adds or updates one secret. Prompts for the value as a hidden input by default, or reads it from stdin so secrets don't end up in shell history. Also writes the value to the project's local `.env` file, so `terse test` runs use the same value that deployed runs will see.

```bash theme={null}
terse secrets add OPENAI_API_KEY
printf '%s' "$OPENAI_API_KEY" | terse secrets add OPENAI_API_KEY --value-stdin
```

| Flag            | Description                      |
| --------------- | -------------------------------- |
| `--value-stdin` | Read the secret value from stdin |

#### `terse secrets remove <NAME>`

Removes one secret. Prompts for confirmation in a terminal; pass `--yes` in scripts. Also deletes the matching line from the project's local `.env` file, so a job that would fail on the next deploy doesn't keep passing locally.

```bash theme={null}
terse secrets remove OPENAI_API_KEY --yes
```

| Flag    | Description                  |
| ------- | ---------------------------- |
| `--yes` | Skip the confirmation prompt |

#### `terse secrets import <file>`

Imports secrets from a `.env`-format file. Skips entries that already exist on the server unless you pass `--overwrite`.

```bash theme={null}
terse secrets import .env
terse secrets import .env --overwrite
```

| Flag          | Description                                         |
| ------------- | --------------------------------------------------- |
| `--overwrite` | Update existing server-side secrets with new values |

### `terse generate`

Fetches your active integrations, their workspace resources, and the current tool definitions from Terse, then writes generated helpers for your local project.

```bash theme={null}
terse generate
```

`terse generate` writes `src/terse.generated.ts`.

What the generated file contains:

| Category                    | Example                                                                 |
| --------------------------- | ----------------------------------------------------------------------- |
| Available integrations      | A header comment listing every integration currently available in Terse |
| Resource constants          | `SlackChannel.Engineering`                                              |
| Trigger builders            | `Triggers.github.onPROpened()`, `Triggers.schedule.cron()`              |
| Skill constructors          | `Skills.github({ repos: [...] })`                                       |
| Deterministic tool wrappers | `toolbox.slack.sendMessage()`, `agent.tools.slack.sendMessage()`        |

`toolbox.*` calls a tool directly with no `TerseAgent` and is not limited by skills. `agent.tools.*` calls the same tool but is limited to skills the agent was created with. See the [Triggers](/reference/triggers) and [Skills](/reference/skills) reference for every helper.

Each integration generates typed helpers from real workspace data such as repositories, Slack channels and members, lists, projects, and tool definitions, so your workflows reference actual resources instead of raw ids.

TypeScript projects also get `Triggers.webhook` from `src/terse.generated.ts`. Pass a type argument to `Triggers.webhook.onRequest<YourBodyType>()` so `onTrigger` and `filter` infer `WebhookTrigger<YourBodyType>` for `event.body`. See [Webhook trigger](/reference/triggers#webhook).

Run `terse generate` again after you connect or disconnect an integration, after resources change inside a connected workspace, or after you upgrade `terse-cli`.

## Improvements

Terse surfaces suggested code patches for your deployed agents based on past runs. These commands let you browse and apply them locally.

### `terse list improvements`

Lists pending improvements grouped by agent.

```bash theme={null}
terse list improvements
```

### `terse apply [improvement-id]`

Downloads the suggested patch and runs `git apply` against your working tree, then marks the improvement as applied on the server. With no id, the CLI prompts you to pick from pending improvements. If a clean apply fails, the CLI tries `git apply --3way`; if that also fails, you can opt into `git apply --reject` to land hunks that do apply cleanly and write `.rej` files for the rest.

```bash theme={null}
terse apply
terse apply imp_abc123
terse apply imp_abc123 --non-interactive
```

Options:

| Flag                    | Description                                                                                                   |
| ----------------------- | ------------------------------------------------------------------------------------------------------------- |
| `-y, --non-interactive` | Fail fast instead of prompting. Requires an improvement id and a clean apply (no `--reject` fallback prompt). |

## Memory

If a job uses the built-in [memory skill](/reference/skills#memory-built-in), it keeps a persistent `/memories` directory that carries over between runs. These commands let you inspect and manage that memory from the terminal without opening the app.

Memory is isolated per project and per job. Every command resolves the job by `--job <name>`, or auto-selects when the project has only one workflow. Pass `--test` to target the isolated memory used by `terse test` runs instead of the production memory a deployed job writes to.

### `terse memory list`

Lists the memory files for a job with their sizes.

```bash theme={null}
terse memory list --job my-job
terse memory list --job my-job --test
terse memory list --job my-job --json
```

Options:

| Flag                  | Description                                                   |
| --------------------- | ------------------------------------------------------------- |
| `--job <name>`        | Job name (auto-selects when only one workflow exists)         |
| `--test`              | Target the isolated `terse test` memory instead of production |
| `--json`              | Emit JSON                                                     |
| `--entry-file <path>` | Override the default workflow entry file                      |

### `terse memory get <path>`

Reads one memory file. Prints to stdout by default, or writes to a local file with `--out`.

```bash theme={null}
terse memory get notes.md --job my-job
terse memory get notes.md --job my-job --out ./notes.md
```

`path` is relative to the job's memory root. The command exits with an error if the file does not exist.

Options:

| Flag                  | Description                                                   |
| --------------------- | ------------------------------------------------------------- |
| `--job <name>`        | Job name (auto-selects when only one workflow exists)         |
| `--test`              | Target the isolated `terse test` memory instead of production |
| `--out <file>`        | Write the contents to a local file instead of stdout          |
| `--entry-file <path>` | Override the default workflow entry file                      |

### `terse memory put <path>`

Creates or replaces a memory file. Reads the contents from `--file`, or from stdin when no file is given.

```bash theme={null}
printf '%s' "new contents" | terse memory put notes.md --job my-job
terse memory put notes.md --job my-job --file ./notes.md
```

Options:

| Flag                  | Description                                                   |
| --------------------- | ------------------------------------------------------------- |
| `--job <name>`        | Job name (auto-selects when only one workflow exists)         |
| `--test`              | Target the isolated `terse test` memory instead of production |
| `--file <path>`       | Local file to upload (otherwise reads stdin)                  |
| `--entry-file <path>` | Override the default workflow entry file                      |

### `terse memory rm <path>`

Deletes one memory file. Prompts for confirmation in a terminal; pass `--yes` in scripts.

```bash theme={null}
terse memory rm notes.md --job my-job --yes
```

Options:

| Flag                  | Description                                                   |
| --------------------- | ------------------------------------------------------------- |
| `--job <name>`        | Job name (auto-selects when only one workflow exists)         |
| `--test`              | Target the isolated `terse test` memory instead of production |
| `--yes`               | Skip the confirmation prompt                                  |
| `--entry-file <path>` | Override the default workflow entry file                      |

## State

If a job declares typed [state](/reference/typescript-sdk#job-state), these commands let you inspect it from the terminal and reset the copy used by `terse test` runs.

Deployed runs and `terse test` runs each have their own state, so testing never pollutes production. Every command resolves the job by `--job <name>`, or auto-selects when the project has only one workflow. Commands default to the deployed state, which is read-only from the CLI; pass `--test` to target the test state. Test state persists across `terse test` invocations until you reset it.

### `terse state list`

Lists the state keys for a job with their sizes.

```bash theme={null}
terse state list --job my-job
terse state list --job my-job --test
terse state list --job my-job --json
```

Options:

| Flag                  | Description                                                      |
| --------------------- | ---------------------------------------------------------------- |
| `--job <name>`        | Job name (auto-selects when only one workflow exists)            |
| `--test`              | Target the isolated `terse test` state instead of deployed state |
| `--json`              | Emit JSON                                                        |
| `--entry-file <path>` | Override the default workflow entry file                         |

### `terse state get <key>`

Prints one state value as JSON. `key` is a key declared in the job's `states`.

```bash theme={null}
terse state get lastProcessedId --job my-job
terse state get lastProcessedId --job my-job --test
```

Options:

| Flag                  | Description                                                      |
| --------------------- | ---------------------------------------------------------------- |
| `--job <name>`        | Job name (auto-selects when only one workflow exists)            |
| `--test`              | Target the isolated `terse test` state instead of deployed state |
| `--entry-file <path>` | Override the default workflow entry file                         |

### `terse state rm <key>`

Deletes one test state key. Deployed state is read-only, so `--test` is required. Prompts for confirmation in a terminal; pass `--yes` in scripts.

```bash theme={null}
terse state rm lastProcessedId --job my-job --test --yes
```

Options:

| Flag                  | Description                                           |
| --------------------- | ----------------------------------------------------- |
| `--job <name>`        | Job name (auto-selects when only one workflow exists) |
| `--test`              | Target the isolated `terse test` state (required)     |
| `--yes`               | Skip the confirmation prompt                          |
| `--entry-file <path>` | Override the default workflow entry file              |

### `terse state reset`

Deletes all test state for a job, the same reset `terse test run --fresh-state` performs before a run. Deployed state is read-only, so `--test` is required.

```bash theme={null}
terse state reset --job my-job --test --yes
```

Options:

| Flag                  | Description                                           |
| --------------------- | ----------------------------------------------------- |
| `--job <name>`        | Job name (auto-selects when only one workflow exists) |
| `--test`              | Target the isolated `terse test` state (required)     |
| `--yes`               | Skip the confirmation prompt                          |
| `--entry-file <path>` | Override the default workflow entry file              |

## Observability

### `terse listen [job-name]`

Streams live trigger events for a deployed workflow and executes the matching local workflow on your machine.

```bash theme={null}
terse listen
terse listen my-workflow
```

What it does:

* Opens an authenticated SSE stream to the backend for your project/workflow
* Prints each forwarded event as it arrives
* Runs your current local workflow code against that serialized trigger

Requirements:

* You’re authenticated (`terse auth login`) or `TERSE_API_KEY` is set
* The workflow is deployed in this project (`terse deploy`)
* `terse.config.json` exists with a valid `projectId`

Options:

| Flag                  | Description                              | Default             |
| --------------------- | ---------------------------------------- | ------------------- |
| `-v, --verbose`       | Show agent stream output during the run  | Enabled             |
| `--no-verbose`        | Hide agent stream output                 |                     |
| `--entry-file <path>` | Override the default workflow entry file | `src/terse.jobs.ts` |

Common errors:

* 401/403 Not authenticated: run `terse auth login`
* 404 Workflow not deployed: run `terse deploy` first

Press Ctrl-C to stop listening.

### `terse replay [run-id]`

Fetches the stored trigger event for one past run and re-executes the matching local workflow on your machine with verbose output.

```bash theme={null}
terse replay run_abc123
```

The CLI resolves the local workflow by the deployed workflow name stored on that run, then executes your current local code against the saved trigger payload.

### `terse history [job-name]`

Lists past runs for a deployed workflow or fetches the full chat history for a single run.

```bash theme={null}
terse history my-job
terse history my-job --json --triggers
terse history --run-id run_abc123
```

When you pass `job-name`, the CLI matches your local workflow name to the deployed workflow on Terse. When you pass `--run-id`, the CLI skips workflow lookup and fetches that run directly.

Options:

| Flag              | Description                                                                                               |
| ----------------- | --------------------------------------------------------------------------------------------------------- |
| `--json`          | Print JSON instead of a table                                                                             |
| `--limit <n>`     | Max runs to return. Default 20, max 100                                                                   |
| `--page <n>`      | Page number, 1-indexed                                                                                    |
| `--status <list>` | Comma-separated statuses: `success`, `failed`, `cancelled`, `skipped`, `in_progress`, `awaiting_approval` |
| `--since <iso>`   | Only runs at or after this ISO timestamp                                                                  |
| `--until <iso>`   | Only runs at or before this ISO timestamp                                                                 |
| `--query <q>`     | Free-text search across trigger, decision, and event fields                                               |
| `--triggers`      | Also fetch the input trigger event JSON for each listed run                                               |
| `--events`        | Also fetch the full model event stream for each listed run                                                |
| `--run-id <id>`   | Show full chat events for one run instead of listing runs for a workflow                                  |

Use `--triggers` when you want the serialized input event for each run (recommended when debugging with the `terse-improve` agent skill). Use `--events` when you want the full stored conversation, including the trigger payload.

### `terse dashboard`

Opens the Terse web app in your default browser.

```bash theme={null}
terse dashboard
```

If `TERSE_FRONTEND_URL` is set, the CLI opens that URL instead of production.

## Authentication

### `terse auth login`

Authenticates with Terse using a device authorization flow and saves your API key to a user-level config file.

```bash theme={null}
terse auth login
```

Opens a browser to confirm the session, then saves an API key to your user config so later commands don't need it inline. If your account belongs to more than one organization, the CLI prompts you to pick which one this CLI session should use.

If a valid API key is already saved, the CLI reports the current user and organization instead of starting a fresh flow.

### `terse auth logout`

Removes saved API keys from your user config.

```bash theme={null}
terse auth logout
```

### `terse auth status`

Shows the user and organization the saved API key belongs to.

```bash theme={null}
terse auth status
```

Exits with code 1 if no credentials are saved or the saved key is expired.

### `terse auth org list`

Lists organizations your account belongs to and marks the active one.

```bash theme={null}
terse auth org list
terse auth org list --json
```

Options:

| Flag     | Description |
| -------- | ----------- |
| `--json` | Emit JSON   |

### `terse auth org switch [org-id]`

Switches the active organization for this CLI session. Tokens for each organization you’ve used are cached locally, so re-switching is instant after the first time.

```bash theme={null}
terse auth org switch
terse auth org switch org_123
```

In non-interactive terminals the org id argument is required.

## Help

### `terse docs`

Opens the Terse documentation site in your default browser.

```bash theme={null}
terse docs
```

If `TERSE_DOCS_URL` is set, the CLI opens that URL instead of the public docs.

### `terse completion install`

Installs shell tab completion for the `terse` binary. Detects bash, zsh, or fish from your `$SHELL`.

```bash theme={null}
terse completion install
```

Open a new shell or source your shell config after installing for completion to take effect. The CLI also offers to install completion automatically the first time you run a command in an interactive terminal.

### `terse completion uninstall`

Removes the tab-completion entries written by `terse completion install`.

```bash theme={null}
terse completion uninstall
```

## API key resolution

Terse uses two credentials, and they are never interchangeable.

`TERSE_API_KEY` holds your **user** token (the same kind you create in the Terse app). Every command that talks to the control plane uses it, and it resolves in this order:

1. `TERSE_API_KEY` in the current process environment (including `.env` loaded from the project root)
2. The API key saved by `terse auth login`

`TERSE_PROJECT_KEY` holds a **project-scoped** token, used only by running workflow code. It reaches runtime routes for one project and cannot deploy, manage integrations, or read organization settings. On the Terse Cloud data plane the control plane injects it into each sandbox automatically, so there is nothing to set. On a self-hosted data plane `terse attach` prints it, and you copy it into your server's environment alongside `TERSE_SIGNING_SECRET`.

Local runs (`terse run`, `terse test`) have no project key, so the CLI falls back to your user token and sets `TERSE_PROJECT_KEY` from it for the duration of the run. The runtime routes accept either.

After `terse init` or `terse auth login`, you usually don't need a project `.env` file for the CLI, because authentication is stored per user on your machine.

`terse secrets` is for your own application secrets (database URLs, third-party API keys); it intentionally refuses any name that starts with `TERSE_`.

To point the CLI at a **self-hosted control plane**, set `TERSE_BACKEND_URL` (default `https://api.useterse.ai`) in your shell before running any `terse` command.
