The outbound CLI
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.
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.
$ 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:
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.
// 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:
--provider <id>if given, otherwise theOUTBOUND_SDK_PROVIDERenvironment variable.- The id is one of
instantly,smartlead,heyreachormemory. - The adapter reads its own key from the environment:
INSTANTLY_API_KEY,SMARTLEAD_API_KEYorHEYREACH_API_KEY.outbound providerslists 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.
# 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 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.
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, with meta on a single read. List commands print a plain array and stop at --limit, which defaults to 50.
import { outbound } from "../cli-commands/cli.ts";
await outbound("tags");
await outbound("tags", "--compact");
$ 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 such as Invalid, NotFound, Unsupported or Unauthorized, or Error for a failure that is not an OutboundError.
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");
$ 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: every command and flag, with its output.
outbound mcp: the same reads served to an agent over stdio.