---
title: The outbound CLI
description: The outbound command reads campaigns, senders, leads, replies and stats from the terminal and prints JSON. How it picks an adapter, what it prints, and how it reports failure.
sidebar:
  label: Overview
related:
  - /docs/cli/commands
  - /docs/cli/mcp
  - /docs/installation
---

The package ships one binary, `outbound`. It is a thin shell over the SDK: each command makes the same read you would make in code and prints the result as JSON. It adds no behaviour of its own, and like the SDK it only reads.

> **Not on npm yet**
>
> `outbound-sdk` is not published, so `npx outbound-sdk` does not work today.
> The binary is available after you install the package from a tarball built in
> the repository. See [installation](/docs/installation).

```text title="Output"
$ outbound help
outbound: read-only access to outbound sending tools

Usage: outbound <command> [options]

  providers
  blocklist [--limit N]
  campaign <id> [--stats] [--sequence] [--subsequences] [--settings] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--by day|total]
  campaigns [--status active|paused|draft|completed|archived] [--limit N]
  capabilities
  health <sender id>
  insight [name] [--input JSON]  computed answers; no name lists them
  leads [--campaign ID | --list ID] [--limit N]
  lists [--limit N]
  limits
  native [name] [--input JSON]   the vendor's own reads; no name lists them
  replies [--campaign ID] [--category positive|negative|neutral|ooo|...] [--since ISO] [--limit N] [--bodies]
  senders [--limit N]
  stats [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--by day|total]
  tags [--limit N]
  thread <id> [--bodies]
  tool <name> [--input JSON]
  tools
  webhooks [--campaign ID] [--limit N]
  mcp                      start a local MCP server on stdio

Options:
  --provider <id>          the adapter to use (default: OUTBOUND_SDK_PROVIDER)
  --compact                print JSON on one line

Each adapter reads its own key from the environment, for example INSTANTLY_API_KEY.

```

That text was printed by the CLI's own code, run by this sample:

```ts title="help.ts"
import { outbound } from "../cli-commands/cli.ts";

await outbound("help");
```

## How the samples run the CLI

Every command shown in this section went through `run`, the function the `outbound` executable calls, exported from `outbound-sdk/cli`. The helper below runs it in the test process with the provider set to `memory`, prints a `$ outbound ...` line, then what the CLI wrote. The executable itself, as a separate process, is covered by the SDK's own tests and is not what these samples start.

```ts title="cli.ts"
// Runs the real CLI in this process, the way the `outbound` executable does,
// and prints what a terminal would show.
import process from "node:process";

import { run } from "outbound-sdk/cli";

// The in-memory adapter with its demo data: no key, no network.
process.env.OUTBOUND_SDK_PROVIDER = "memory";

const quote = (argument: string): string =>
  /^[\w./:=-]+$/u.test(argument) ? argument : `'${argument}'`;

const execute = async (argv: readonly string[]) => {
  const lines: string[] = [];

  const code = await run(argv, {
    stderr: (text) => {
      lines.push(text);
    },
    stdout: (text) => {
      lines.push(text);
    },
  });

  console.log(
    `$ outbound ${argv.map((argument) => quote(argument)).join(" ")}`
  );

  return { code, text: lines.join("\n") };
};

/** Runs `outbound <argv>`, then prints the command, its output and a failing exit code. */
export const outbound = async (...argv: string[]): Promise<void> => {
  const { code, text } = await execute(argv);
  console.log(text);

  if (code !== 0) {
    console.log(`(exit code ${code})`);
  }

  console.log("");
};

/**
 * For output too long to show: runs the command and prints, in brackets, what
 * `describe` says about the output instead of the output itself.
 */
export const outboundSummary = async (
  argv: readonly string[],
  describe: (text: string) => string
): Promise<void> => {
  const { text } = await execute(argv);
  console.log(`[${describe(text)}]\n`);
};
```

`run(argv, io)` takes the arguments after `outbound` and an object with `stdout` and `stderr` functions, and resolves to the exit code. You can call it the same way to embed the CLI in a script. `io.stdin` supplies lines for `outbound mcp`, and `io.adapter` replaces the adapter the environment would choose, which is meant for tests.

## Choosing the provider

A command that reads data needs an adapter. The CLI builds one from the environment:

1. `--provider <id>` if given, otherwise the `OUTBOUND_SDK_PROVIDER` environment variable.
2. The id is one of `instantly`, `smartlead`, `heyreach` or `memory`.
3. The adapter reads its own key from the environment: `INSTANTLY_API_KEY`, `SMARTLEAD_API_KEY` or `HEYREACH_API_KEY`. `outbound providers` lists the variables each one reads.

`memory` needs no key. It runs on a small built-in demo data set, the same one behind `demoData()` in `outbound-sdk/memory`: two campaigns, three leads, a few messages. Use it to try a command before pointing the CLI at a real account.

```sh
# a real account
export OUTBOUND_SDK_PROVIDER=instantly
export INSTANTLY_API_KEY=...
outbound campaigns --status active

# the demo data, for one command
outbound campaigns --provider memory
```

Those shell lines are not run by the test suite. The CLI takes no key on the command line and reads no configuration file. It builds a plain instance with no [plugins](/docs/plugins) and one adapter, so there is no CLI flag for a cache, redaction or several clients. How far each real adapter has been checked is on [what is verified](/docs/status).

`help` and `providers` need no adapter and work with nothing set.

## Output

A command prints one JSON value on standard output: indented by default, on one line with `--compact`. Records are the SDK's [records](/docs/api/records), with `meta` on a single read. List commands print a plain array and stop at `--limit`, which defaults to 50.

```ts title="output.ts"
import { outbound } from "../cli-commands/cli.ts";

await outbound("tags");

await outbound("tags", "--compact");
```

```text title="Output"
$ outbound tags
[
  {
    "provider": "memory",
    "raw": null,
    "ref": "memory:default:tag_a",
    "schemaVersion": 1,
    "sourceId": "tag_a",
    "workspaceId": "default",
    "description": "Work for client A",
    "name": "client-a"
  }
]

$ outbound tags --compact
[{"provider":"memory","raw":null,"ref":"memory:default:tag_a","schemaVersion":1,"sourceId":"tag_a","workspaceId":"default","description":"Work for client A","name":"client-a"}]

```

## Exit codes and errors

| Exit code | When |
| --- | --- |
| `0` | The command ran and got its answer. |
| `1` | Anything else: an unknown command or option, a missing argument, a flag value that is not one of the allowed words, a record that does not exist, a read the adapter cannot serve, a rejected key, or a tool whose result is `{ "ok": false }`. |

On failure nothing is written to standard output, with one exception: `outbound tool` prints the tool's `{ "ok": false }` result there and writes nothing to standard error. Standard error gets `<code>: <message>`, where the code is an [`OutboundError` code](/docs/api/errors) such as `Invalid`, `NotFound`, `Unsupported` or `Unauthorized`, or `Error` for a failure that is not an `OutboundError`.

```ts title="errors.ts"
import process from "node:process";

import { outbound } from "../cli-commands/cli.ts";

await outbound("campaign", "no-such-campaign");

await outbound("campaign");

await outbound("campaigns", "--limit", "0");

// A flag that takes a fixed word names the words it takes.
await outbound("campaigns", "--status", "live");

await outbound("stats", "--by", "week");

await outbound("tool", "sender_health", "--input", "{oops");

await outbound("native", "inboxUnreadCount");

await outbound("send");

await outbound("campaigns", "--provider", "mailchimp");

// With no provider chosen, a command that needs an adapter says what to set.
delete process.env.OUTBOUND_SDK_PROVIDER;

console.log("$ unset OUTBOUND_SDK_PROVIDER");

await outbound("campaigns");

// `providers` and `help` need no adapter, so they still work.
await outbound("providers", "--compact");
```

```text title="Output"
$ outbound campaign no-such-campaign
NotFound: Campaign no-such-campaign was not found
(exit code 1)

$ outbound campaign
Invalid: Missing a campaign id
(exit code 1)

$ outbound campaigns --limit 0
Invalid: --limit must be a positive whole number
(exit code 1)

$ outbound campaigns --status live
Invalid: --status must be one of: active, archived, completed, draft, paused, unknown
(exit code 1)

$ outbound stats --by week
Invalid: --by must be one of: day, total
(exit code 1)

$ outbound tool sender_health --input '{oops'
Invalid: --input must be valid JSON
(exit code 1)

$ outbound native inboxUnreadCount
Unsupported: Reading the vendor's own data is not available: the memory adapter does not support it (native)
(exit code 1)

$ outbound send
Invalid: Unknown command "send". Run "outbound help"
(exit code 1)

$ outbound campaigns --provider mailchimp
Invalid: Unknown provider "mailchimp". Known providers: heyreach, instantly, memory, smartlead
(exit code 1)

$ unset OUTBOUND_SDK_PROVIDER
$ outbound campaigns
Invalid: Set OUTBOUND_SDK_PROVIDER to one of: instantly, smartlead, heyreach, memory
(exit code 1)

$ outbound providers --compact
[{"channels":["email"],"envVars":[{"name":"INSTANTLY_API_KEY","required":true},{"name":"INSTANTLY_WORKSPACE_ID","required":false}],"id":"instantly","maturity":"beta","name":"Instantly","note":"Its reads were run, read-only, against one live workspace. Fixtures are built from the documentation."},{"channels":["email"],"envVars":[{"name":"SMARTLEAD_API_KEY","required":true},{"name":"SMARTLEAD_WORKSPACE_ID","required":false}],"id":"smartlead","maturity":"unverified","name":"Smartlead","note":"Never run against a live account. Written from the public documentation and tested offline only."},{"channels":["linkedin"],"envVars":[{"name":"HEYREACH_API_KEY","required":true},{"name":"HEYREACH_WORKSPACE_ID","required":false}],"id":"heyreach","maturity":"beta","name":"HeyReach","note":"Its reads were run, read-only, against one live workspace. Fixtures are built from the documentation."},{"channels":["email","linkedin"],"envVars":[],"id":"memory","maturity":"stable","name":"In-memory (tests and demos)","note":"Holds data in memory. For tests and demos; there is no vendor behind it."}]

```

The helper prints standard error where a terminal would show it and adds the `(exit code 1)` line. That line is not CLI output.

## Next

- [Commands](/docs/cli/commands): every command and flag, with its output.
- [`outbound mcp`](/docs/cli/mcp): the same reads served to an agent over stdio.
