---
title: "Outbound From the Terminal: A Practical CLI Runbook"
description: "Run outbound from the terminal with the FirstSales CLI: setup, profiles, a safe loop, scripts, exit codes and what stays in the app."
date: "2026-09-30"
tags: "cli, terminal, outbound, automation, ai-agents, runbook"
readTime: "28 min read"
author: "FirstSales Team"
slug: "outbound-from-the-terminal"
canonical: "https://firstsales.io/blog/outbound-from-the-terminal/"
---

# Running outbound from the terminal

**TL;DR:** The terminal suits the repeatable half of outbound: checks, reports, imports and previews. Set up the FirstSales CLI with a narrow key, follow the inspect, mutate, verify loop, rehearse every write with `--dry-run`, and leave judgment calls to a person in the app.

---

## Table of contents

- [Why run outbound from a terminal at all](#why-run-outbound-from-a-terminal-at-all)
- [The point of view: the terminal is for the boring half](#the-point-of-view-the-terminal-is-for-the-boring-half)
- [Day one: install, authenticate, verify](#day-one-install-authenticate-verify)
- [Profiles, environment variables and several workspaces](#profiles-environment-variables-and-several-workspaces)
- [The loop: inspect, mutate, verify](#the-loop-inspect-mutate-verify)
- [Reading output: JSON, tables and pipes](#reading-output-json-tables-and-pipes)
- [Dry runs and idempotency keys](#dry-runs-and-idempotency-keys)
- [Exit codes in scripts](#exit-codes-in-scripts)
- [A weekly operating rhythm](#a-weekly-operating-rhythm)
- [Launching a campaign from the terminal, safely](#launching-a-campaign-from-the-terminal-safely)
- [Working with a coding agent](#working-with-a-coding-agent)
- [Scripts worth writing first](#scripts-worth-writing-first)
- [What to leave in the app](#what-to-leave-in-the-app)
- [Mistakes that cost the most](#mistakes-that-cost-the-most)
- [A thirty-day adoption plan](#a-thirty-day-adoption-plan)
- [Comparing ways of working](#comparing-ways-of-working)
- [FAQs](#faqs)
- [Conclusion](#conclusion)

## Why run outbound from a terminal at all

Most outbound work is not creative. It is checking, copying, comparing and clicking the same buttons in the same order.

A person is good at the creative part and poor at the rest. After the fortieth check of the week, attention slips, and a wrong workspace or a skipped step creeps in.

The command line is where repeatable work lives. A command can be written down, reviewed, scheduled, logged and repeated by someone else next week.

FirstSales ships a CLI for this. The package `@firstsales.io/cli` installs a binary named `firstsales`, and its README describes it as a thin, JSON-first layer over the public Developer API.

This post is a runbook. It takes you from a clean machine to a weekly rhythm, with the safety steps in place.

It does not tell you to move everything to the terminal. Part of the skill is knowing what to leave in the app, and a section below covers that.

If you are still deciding between surfaces, read [CLI, MCP or the app](/blog/cli-vs-mcp-vs-app-outbound) first. If you already know you want the terminal, carry on.

## The point of view: the terminal is for the boring half

Here is the stance this post argues for. Run the boring half of outbound from the terminal, and keep the thinking half in front of a person.

Boring does not mean unimportant. A report that checks bounce movement every Monday is boring and valuable. A suppression check before every import is boring and protects your domains.

Thinking work is different. Choosing an audience, judging a reply and deciding what to say are tasks where context on a screen helps and a script does not.

The Unix tradition behind the command line states a similar split. Doug McIlroy's often-cited summary, recorded on the [Unix philosophy page](https://en.wikipedia.org/wiki/Unix_philosophy), says to make each program do one thing well, to make programs work together, and to use text as the common interface. Paraphrased: small tools, joined by plain text.

The FirstSales CLI follows that shape. Each command does one small thing and prints text that the next tool can read.

That is why the terminal suits outbound hygiene. You compose small, checkable steps instead of trusting one large clever one.

## Day one: install, authenticate, verify

Start with a machine you control and Node 20 or later, which the README lists as the requirement.

### Install

```bash
npm install -g @firstsales.io/cli
firstsales --help
```

Check the version after install, since the CLI moves quickly.

```bash
firstsales --version
```

### Create a key for yourself

Open the app or use an existing owner key to create a personal key with the access you need. Prefer workspace level and only the scopes your tasks require.

```bash
firstsales api-keys create \
  --org org_123 \
  --data '{"name":"terminal-udit-ws123-202610","scopes":["campaigns:read"],"accessLevel":"workspace"}' \
  --json
```

Replace the name with your own. The raw key prints once, so copy it into your secret manager at that moment.

Do not add `--idempotency-key` to this command, because the README says it is unsupported for `api-keys create`. And never paste a key into a chat with an AI tool.

The post on [API keys for outbound agents](/blog/api-keys-for-outbound-agents) explains how to size keys. For your own terminal, start read-only and widen one scope at a time as real tasks need it.

### Authenticate

The CLI looks for a key in this order: the `--api-key` flag, the `FIRSTSALES_API_KEY` environment variable, then a saved profile.

For a quick session, export the variable in your shell.

```bash
export FIRSTSALES_API_KEY="<YOUR_FIRSTSALES_API_KEY>"
```

Type the real value from your secret manager. Do not put it in a script file or a command that lands in shared history.

### Verify identity

```bash
firstsales whoami --json
```

The README compares `whoami` to `npm whoami`. It validates the token and prints the user, key, organization and workspace context, and does not mutate anything.

Read the output. Check the organization and workspace are the ones you meant.

### Check setup

```bash
firstsales doctor --pretty
```

`doctor` checks your setup. Run it whenever something behaves oddly, before you start debugging anything else.

### First reads

Find your organization and workspace ids, and list your campaigns.

```bash
firstsales orgs list --json
firstsales workspaces list --org org_123 --json
firstsales campaigns list --org org_123 --workspace ws_123 --json
```

Use `--pretty` the first time, to see the real output shape. This post avoids naming response fields, because the live output is the authority.

## Profiles, environment variables and several workspaces

If you manage more than one workspace or organization, you need a way to switch without mistakes.

### Named profiles for a laptop

The CLI can store named profiles locally.

```bash
firstsales auth login --profile prod --base-url "https://api.app.firstsales.io"
firstsales auth status --profile prod --json
firstsales auth logout --profile prod
```

The README shows `--api-key` on the login command. Prefer entering the key through a prompt or a secrets tool if your version supports it, and check `firstsales auth login --help` for the options on your installed version.

Select a profile on any command with `--profile <name>`, or set `FIRSTSALES_PROFILE`. `auth status` never prints the raw key.

### Environment variables for automation

For scripts and CI, prefer environment variables. The README says the CLI can read local profiles, but automation should prefer variables, and you should not commit profile files or keys.

Other variables are `FIRSTSALES_BASE_URL`, which defaults to `https://api.app.firstsales.io`, and `FIRSTSALES_PROFILE`.

### Make the target visible

The most common terminal mistake is acting on the wrong workspace. Make the target visible in every command by passing `--org` and `--workspace` explicitly, and consider a prompt that shows the active profile.

```bash
alias fsq='firstsales whoami --json'
```

Run a check like that at the start of a session and before any write. A second of looking beats an hour of cleanup.

### One key per task, not one per person

A person who runs three kinds of task should hold three keys, not one wide one. Switch with profiles.

A read-only profile for daily checks stays loaded. A write profile is used only for the write, and logged out afterwards.

## The loop: inspect, mutate, verify

The README distills a loop for agents and scripts, and it works for humans too.

```text
1. firstsales whoami --json
2. Read org/workspace/resource state.
3. Plan the smallest mutation.
4. Use --dry-run when useful.
5. Use --idempotency-key only for supported retryable mutations; never for api-keys create.
6. Use --confirm only for intentional destructive commands.
7. Re-read state and verify the result.
```

Its motto is "Inspect first. Mutate deliberately. Verify after every action."

### Why each step exists

**Identity first** prevents wrong-workspace errors. **Reading state** gives you a before picture to compare with.

**The smallest mutation** limits the blast radius. Change one list or one campaign, not twenty.

**A dry run** shows exactly what would be sent. **An idempotency key** makes a retry safe when a network call times out.

**Confirm only for deletes** keeps a pause before the irreversible. **Re-reading** proves the change happened, instead of trusting a success message.

### The loop in practice

Say you want to add a list to a campaign's workflow. Here is the loop with real commands.

```bash
# 1 and 2: identity and before state
firstsales whoami --json
firstsales campaigns workflow get --campaign camp_123 --org org_123 --workspace ws_123 --pretty

# 3 and 4: smallest change, previewed
firstsales campaigns workflow update --campaign camp_123 --org org_123 --workspace ws_123 \
  --include-list list_123 --dry-run

# apply only after the preview looks right
firstsales campaigns workflow update --campaign camp_123 --org org_123 --workspace ws_123 \
  --include-list list_123 --json

# 7: after state
firstsales campaigns workflow get --campaign camp_123 --org org_123 --workspace ws_123 --pretty
```

The README says `workflow update` sends one PATCH with only the ids you give, and that you can repeat `--include-list` for several lists. It also takes `--sender` for sender connector ids.

Save the before and after output to files. A plain diff of the two is a record of what changed.

### Keep the loop in your head

The loop is short enough to memorize. When something feels rushed, say the steps in order. Most expensive mistakes skip step 1, 4 or 7.

## Reading output: JSON, tables and pipes

The CLI is built to be read by machines, and that is a feature for you too.

### Output formats

The README lists `--output json|table|tsv`. It defaults to a table on a terminal and JSON when piped.

Pass `--json` for compact JSON and `--pretty` for indented JSON. Use `--output tsv` when you want columns you can feed to other tools.

### jq for filtering

The [jq manual](https://jqlang.org/manual/) covers the filter language. A few patterns carry most of the work.

```bash
# count items in a list response
firstsales campaigns list --org org_123 --workspace ws_123 --json | jq 'length'

# see the top-level keys so you know the shape
firstsales campaigns list --org org_123 --workspace ws_123 --json | jq 'keys'
```

The first example assumes the response is an array, and the second works on an object. Check the shape with `--pretty` once, and write the filter that matches it.

### Pagination

List commands take `--page` and `--limit`, and `--all` fetches every page. Use `--all` for reports and a limit for quick looks.

### Save before you transform

Write raw output to a dated file first, then transform the file. If a filter is wrong, you still have the original.

```bash
mkdir -p ~/outbound/raw
firstsales campaigns list --org org_123 --workspace ws_123 --all --json \
  > ~/outbound/raw/campaigns-$(date +%F).json
```

This habit is the base of the weekly report approach in [weekly campaign reports with cron and the CLI](/blog/weekly-campaign-report-cron-cli).

### Do not parse what you have not looked at

Never write a script against a field you have not seen in real output. Run the command, read it, then write the filter.

Fields can change between CLI releases. A field-agnostic comparison, as in the weekly report post, survives changes better than a hard-coded path.

## Dry runs and idempotency keys

Two flags do most of the safety work, and they deserve their own section.

### What `--dry-run` does

The README says `--dry-run` previews the HTTP request without auth or network calls. It prints the method, URL, redacted headers and body.

That makes it safe to use anywhere, including on a machine with no key at all. It is also a way to learn the shape of a request.

```bash
firstsales contacts create --org org_123 --workspace ws_123 \
  --data '{"email":"alex@example.com","firstName":"Alex"}' \
  --idempotency-key demo-001 --dry-run
```

Read the printed request. Check the path, the workspace and the body before you run it for real.

### What `--idempotency-key` does

An idempotency key tells the API that repeated requests with the same key are the same request. A retry after a timeout then does not create a duplicate.

Use it on retryable writes. The README example builds keys from the data, such as `crm-import-alex@example.com`.

Never use it on `api-keys create`. And remember that `blocked-domains add` requires an idempotency key, and the CLI will not make one up for you.

### Build keys that mean something

Make keys from the thing being done, not random strings. `import-2026-10-08-row-0042` tells you what the key protected.

A meaningful key helps in a log and prevents accidental reuse across different operations.

### `--confirm` for destructive commands

Destructive commands stop unless you pass `--confirm`. If you see `destructive_confirmation_required`, the guard worked, so read the command again.

`contacts delete` and other delete commands cannot be undone. Rehearse them with `--dry-run`, and keep them out of scripts that run unattended.

## Exit codes in scripts

A terminal job is only as good as its failure handling. The CLI gives you stable exit codes to branch on.

| Exit code | Meaning | What a script should do |
|---|---|---|
| 0 | ok | continue |
| 1 | runtime error | log and decide per task |
| 2 | usage error | stop, the command is wrong |
| 3 | auth error | stop and alert: key bad or revoked |
| 4 | not found | log, an id may be wrong |
| 5 | rate limited | wait, retry once, then log |

The README notes that exit codes were redefined in version 0.1.1, so scripts written for the older scheme need migrating.

### A small wrapper

Wrap calls in a function that returns the code and writes failures to a log.

```bash
fs() {
  firstsales "$@" --org "$ORG_ID" --workspace "$WORKSPACE_ID" --json
}

out=$(fs campaigns progress --campaign camp_123 2>>"$LOG") && rc=0 || rc=$?
if [ "$rc" -ne 0 ]; then
  case $rc in
    3) echo "auth failed" >> "$LOG"; exit 3 ;;
    5) sleep 30; out=$(fs campaigns progress --campaign camp_123) ;;
    *) echo "failed rc=$rc" >> "$LOG" ;;
  esac
fi
```

Errors come back as JSON that scripts can parse. The README lists stable error names such as `insufficient_scope`, `missing_api_key`, `invalid_api_key` and `unsupported_operation`.

### Treat unsupported as a stop

If the CLI returns `unsupported_operation`, stop and report that the capability is not public. Signals and webhooks fall in this group today.

Do not work around it by calling app-private routes. The README says only `/api/v1` endpoints are supported.

### The escape hatch

For a public endpoint with no dedicated command, use `firstsales api GET /api/v1/...`. It stays on the public surface.

Check the local command registry first with `firstsales commands --json`, since the command may already exist.

## A weekly operating rhythm

Tools help most when they sit inside a rhythm. Here is a weekly cadence that splits the work between terminal and app.

```mermaid
graph TD
  M[Monday 07:00 cron: weekly report] --> M2[Manager reads diff]
  D[Daily 08:30: read-only standup] --> D2[Decide today's actions in the app]
  W[Wednesday: import and suppression scripts] --> W2[Review list, then attach to campaign]
  T[Thursday: launch window] --> T2[Readiness, test email, dry run, start]
  F[Friday: key and alert review] --> F2[Rotate or revoke, ack alerts]
  M2 --> D
  D2 --> W
  W2 --> T
  T2 --> F
```

### Daily: a read-only check

Each morning, run a short read-only check. The [daily outbound standup prompt](/blog/daily-outbound-standup-prompt) shows a version that ends in a decision list.

Without an agent, a few commands do the same job.

```bash
firstsales whoami --json
firstsales connectors list --org org_123 --workspace ws_123 --pretty
firstsales alerts list --org org_123 --workspace ws_123 --pretty
firstsales inbox threads --org org_123 --workspace ws_123 --pretty
```

Read connector status, alerts and new threads. Decide the day's actions, then act in the app.

### Weekly: a reproducible report

Once a week, a cron job saves snapshots and diffs them against the week before. Numbers come from the script, and a person reads the diff.

The full build is in [weekly campaign reports with cron and the CLI](/blog/weekly-campaign-report-cron-cli).

### As needed: imports with a gate

When new leads arrive, check suppression before you import.

```bash
firstsales suppression check alex@example.com sam@example.com \
  --org org_123 --workspace ws_123 --json
```

The command accepts up to 100 values at a time. For imports, see [import leads from the terminal](/blog/import-leads-from-terminal), and verify addresses first, as covered in [email verification before sending](/blog/email-verification-before-sending).

### Weekly: a key and alert review

Once a week, list keys, read usage and clear alerts.

```bash
firstsales api-keys list --org org_123 --pretty
firstsales usage get --org org_123 --pretty
firstsales alerts list --org org_123 --workspace ws_123 --pretty
```

Revoke any key nobody can explain. Acknowledge or resolve alerts that are handled, using `alerts ack` and `alerts resolve`.

### Sender health

Check the sender side whenever bounces rise. `connectors test` tests a connector, and `warmup status` reads warmup state for a connector.

```bash
firstsales connectors test --org org_123 --workspace ws_123 --connector conn_123 --pretty
firstsales warmup status --org org_123 --workspace ws_123 --connector conn_123 --pretty
```

Connector responses never return provider secrets. For a bounce playbook, see [cold email bounce rate](/blog/cold-email-bounce-rate) and [deliverability incident response](/blog/deliverability-incident-response).

Google's [sender guidelines](https://support.google.com/a/answer/81126) say to keep the spam rate reported in Postmaster Tools below 0.10% and avoid reaching 0.30%. The terminal cannot read that number, so check Postmaster Tools alongside your weekly diff.

## Launching a campaign from the terminal, safely

Launching is the step where the terminal's guards matter most. Follow the order every time.

### Step 1: create

Creating a campaign is a CLI command.

```bash
firstsales campaigns create --org org_123 --workspace ws_123 --data-file campaign.json --dry-run
```

The README does not document the body of `campaigns create`, so check `firstsales campaigns create --help` and the Developer API docs for the fields. Preview with `--dry-run`, then create.

MCP campaign creation is not confirmed, so do not plan to create a campaign through an agent over MCP.

### Step 2: prepare

Attach lists and senders with `campaigns workflow update`. Preview first.

### Step 3: readiness

Run the readiness check.

```bash
firstsales campaigns readiness --org org_123 --workspace ws_123 --campaign camp_123 --pretty
```

Read every line. The README does not document the extra flags, so use `--help` on your installed version. See [the campaign readiness check from the CLI](/blog/campaign-readiness-check-cli).

### Step 4: send a test email

```bash
firstsales campaigns send-test-email --org org_123 --workspace ws_123 --campaign camp_123 --pretty
```

Open the test email as the recipient would. Check the subject, the first line, links and the unsubscribe path. The post on [sending a test email with Claude Code](/blog/send-test-email-claude-code) covers what to look for.

### Step 5: preview the start

`campaigns start` needs a JSON body with `savedVersionId`, `readinessVersion`, `idempotencyKey` and `confirmation`.

```bash
firstsales campaigns start --org org_123 --workspace ws_123 --campaign camp_123 \
  --data-file launch.json --dry-run
```

Read the preview. Confirm the campaign id, the workspace and the body.

### Step 6: start, then verify

Run the start without `--dry-run` only when the preview is right. Then re-read progress.

```bash
firstsales campaigns progress --org org_123 --workspace ws_123 --campaign camp_123 --pretty
```

The full sequence, with pause and resume, is in [start, pause and resume campaigns from the CLI](/blog/start-pause-campaigns-cli).

### Keep a stop within reach

Know how to pause before you start. `campaigns pause` and `campaigns resume` exist for this.

If bounces jump in the first hour, pause first and investigate after. A paused campaign costs a few hours, and a runaway one costs reputation.

## Working with a coding agent

A coding agent with a shell, such as Claude Code, can run the CLI for you. This is where the safety design earns its place.

### The package ships skills

The package includes skills for Claude Code named `firstsales-cli`, `firstsales-product-mcp` and `firstsales-developer-api`. The CLI skill gives the agent a short rulebook.

It says to verify identity first, read state before mutation, prefer JSON, use dry runs before risky writes, use idempotency keys only for retryable mutations, use `--confirm` only for intentional destructive commands, and re-read state afterwards.

It also says to stop on `unsupported_operation`, and not to invent commands. `firstsales commands --json` is the authority.

### Limit what the agent can do

The agent inherits your key. So give it a key sized to the task, and do not hand it your owner key.

Run it with a read-only key for investigation. Widen only for a specific write, then switch back.

The Claude Code [headless mode](https://code.claude.com/docs/en/headless) runs a prompt non-interactively with `claude -p`, and lets you limit tools with `--allowedTools`. Use that to keep an unattended run narrow.

### Keep the key out of the prompt

Never paste a key into a prompt. Export the variable in the shell where the agent runs, or let a wrapper load it, and the agent will use the CLI without ever seeing the value.

### Untrusted text

Reply bodies and prospect data are written by strangers. An agent reading them can be steered by instructions hidden inside.

Keep write power away from an agent that reads replies. The [MCP security for outbound](/blog/mcp-security-outbound) and [email agent guardrails](/blog/email-agent-guardrails) posts explain the pattern.

### Good tasks for an agent in the terminal

Agents do well at reading and explaining. Ask for summaries of a report diff, a list of campaigns with rising bounces, or a draft of a script you then review.

They do less well at unsupervised writes. Ask for a plan and a dry-run preview, then run the real command yourself.

### Use the Copilot command for narration

`firstsales copilot ask "<prompt>"` runs the product's assistant non-interactively and prints only the reply text on standard output. It is safe to pipe.

Use it for a quick read of live state. For numbers that must match an archive, produce them with the CLI and narrate afterwards.

## Scripts worth writing first

If you write only a few scripts, write these, in this order.

### 1. The identity guard

A tiny function that runs `whoami` and compares the organization and workspace to expected values. Source it at the top of every other script.

```bash
guard() {
  out=$(firstsales whoami --json) || exit 3
  echo "$out" | grep -q "$WORKSPACE_ID" || { echo "wrong workspace" >&2; exit 2; }
}
```

The `grep` is a blunt check. Replace it with a `jq` comparison after you have seen the real output shape.

### 2. The morning check

The read-only daily commands, saved to a dated file. It takes ten minutes to write and saves ten minutes each day.

### 3. The weekly snapshot and diff

The cron job from the weekly report post. This is the highest-value script for most teams.

### 4. The suppression gate

A script that takes a file of addresses, checks them in batches of 100, and writes the clear ones to a new file. Import only from the clear file.

```bash
split -l 100 addresses.txt batch_
for f in batch_*; do
  firstsales suppression check $(cat "$f") --org "$ORG_ID" --workspace "$WORKSPACE_ID" --json
done
```

Parse the output after checking its real shape. Never import an address whose check failed.

### 5. The import with idempotency keys

A loop that creates contacts with keys built from the row. A rerun after a crash then does not duplicate anyone.

For incoming events, see [start a campaign when a buying signal fires](/blog/signal-triggered-campaign-webhook), which shows a receiver built from the same parts.

### Keep scripts boring

A script should do one thing, print what it did and exit with a meaningful code. Resist clever features, since you will read these scripts at the worst moments.

### Put scripts under review

Treat scripts like code. Keep them in version control without keys, review changes, and note which key each one uses.

## What to leave in the app

The terminal is not a better app. Some work is better in the app, and knowing which saves trouble.

### Judgment with context

Reading a thread next to the lead's history, comparing two sequences side by side, or deciding who deserves a personal note is faster on a screen.

### Setup you do rarely

The first mailbox, the first sending domain, a new workspace and billing changes are rare tasks. Clicking through teaches you what the pieces are.

### People and permissions

Inviting teammates, changing roles and approving access belong where the team is. The app gives everyone the same view.

### Anything the CLI marks unsupported

Signals and webhooks return `unsupported_operation` today. Use the app for them, and do not reach for private routes.

### Final approval of risky actions

Even when a script prepares a launch, let a person click the final start or read the final preview. The human gate is cheap, and the cost of a bad send is not.

An email sent by an API key also waits for Send Approval, according to the CLI documentation on `emails send`, `schedule` and `draft`. That is a backstop, and it should not replace your own gate.

## Mistakes that cost the most

These are the errors that show up most often when teams move work to the terminal.

### Acting in the wrong workspace

Fix: `whoami` first, explicit `--org` and `--workspace` flags, and an identity guard in scripts.

### Pasting a key into a prompt or a ticket

Fix: environment variables and secret managers only. If a key was pasted, revoke it at once and create a new one.

### Running a write without a dry run

Fix: preview every write the first time. A `--dry-run` costs a second and has no side effects.

### Putting deletes in unattended scripts

`contacts delete` and other delete commands cannot be undone. Fix: keep deletes manual, with a person reading each command.

### Scheduling a job with a key that dies

Keys fail closed if their creator loses organization access or is suspended. Fix: record creators in a key register and re-issue before someone leaves.

### Parsing fields you never looked at

Fix: read the real output with `--pretty` and use field-agnostic comparisons where you can.

### Starting without readiness and a test email

Fix: run `campaigns readiness` and `campaigns send-test-email` before every `campaigns start`, with no exceptions.

### Giving an agent a wide key

Fix: one narrow key per job, read-only by default.

### Ignoring exit codes

Fix: branch on codes 3 and 5 on purpose, and alert on failure. A job that fails silently is worse than none.

## A thirty-day adoption plan

If your team is new to this, move in stages. Each stage should produce something useful.

### Week 1: read only

Install the CLI, create read-only keys, and run `whoami` and `doctor`. Build the morning check script and use it daily.

Goal: everyone on the team can read their workspace from the terminal and trusts the output.

### Week 2: reports

Build the weekly snapshot and diff, schedule it with cron and add the dead-man's check. Send the first report to the team.

Goal: one report that people read, produced without anyone opening the app.

### Week 3: gated writes

Add the suppression gate and the import script with idempotency keys. Use `--dry-run` on every write the first time.

Goal: a lead import that is safe to rerun.

### Week 4: launch discipline

Practice the launch sequence on a test campaign: create, prepare, readiness, test email, dry-run start. Write the sequence as a checklist.

Goal: a launch that any team member can run the same way.

### After day 30

Review keys, prune unused ones and rotate old ones. Add an agent for reading and explaining, with a read-only key.

Revisit the choice of surface every quarter. See [CLI, MCP or the app](/blog/cli-vs-mcp-vs-app-outbound) for the matrix.

### Measure whether it helps

Track two numbers: the hours spent each week on routine checks, and the count of avoidable mistakes such as wrong-workspace actions or skipped steps. If both fall, the terminal is paying off.

If routine work does not shrink after a month, simplify. You may have automated the wrong things, or built scripts nobody reads.

Pair the plan with an audit habit too. [Outbound process audit](/blog/outbound-process-audit) and monitoring outbound agents give you ways to review how the whole system behaves.

## Comparing ways of working

This table compares three ways a team might run outbound hygiene.

| Way of working | Repeatable | Logged | Scales to many campaigns | Safe against slips |
|---|---|---|---|---|
| Click through the app each time | ✗ | ✗ | ✗ | ✗ |
| Ad hoc commands typed by whoever is free | ✗ | ✗ | ✓ | ✗ |
| Reviewed scripts with narrow keys, dry runs and exit-code handling | ✓ | ✓ | ✓ | ✓ |
| Reviewed scripts plus a read-only agent for explanation | ✓ | ✓ | ✓ | ✓ |

The second row is where many technical teams sit. Typed commands are fast and flexible, and they leave no record and no checks.

The third row costs a few days of setup. It pays back the first time a script catches a wrong workspace or a skipped readiness step.

### Where the agent adds value

The fourth row adds an agent for explaining and exploring. It does not add any new write power.

That is the right shape. Let scripts do the work, and let the agent help you understand the results.

## FAQs

### Can I run outbound entirely from the terminal?

You can run much of it, such as checks, reports, imports and launches. Signals and webhooks are not supported by the CLI today, and judgment calls are better in the app.

### How do I install the FirstSales CLI?

Run `npm install -g @firstsales.io/cli` with Node 20 or later. The binary is named `firstsales`.

### How do I authenticate?

Set `FIRSTSALES_API_KEY` in your shell, pass `--api-key`, or use a saved profile. The CLI checks them in that order.

### Where do I get an API key?

Create one with `firstsales api-keys create`, using a scope list and access level that fit your task. The raw key prints once.

### How do I check which workspace I am in?

Run `firstsales whoami --json`. It prints the user, key, organization and workspace context without changing anything.

### What does `doctor` do?

It checks your setup. Run `firstsales doctor --pretty` when something behaves oddly.

### What is the safe loop for any change?

Verify identity, read state, plan the smallest change, preview with `--dry-run`, apply with an idempotency key where supported, and re-read to verify.

### What does `--dry-run` do?

It previews the HTTP request with redacted headers and the body, and makes no auth or network calls. It is safe to run anywhere.

### When do I need `--confirm`?

For destructive commands such as delete and revoke. The CLI stops without it, and you add it only when the action is intentional.

### Are delete commands reversible?

No. `contacts delete` and other delete commands cannot be undone, so keep them out of unattended scripts.

### What exit codes should my scripts handle?

`0` ok, `1` runtime error, `2` usage error, `3` auth error, `4` not found and `5` rate limited. Alert on 3 and retry once on 5.

### How do I filter output?

Use `--pretty` to see the shape, then filter with `jq`. Use `--output tsv` for columns, and `--all` to fetch every page.

### Which commands should I run every morning?

`whoami`, `connectors list`, `alerts list` and `inbox threads`, all read-only. They show sender health, open alerts and new replies.

### What must I do before `campaigns start`?

Run `campaigns readiness` and `campaigns send-test-email`, read the test email, and preview the start with `--dry-run`. Do this every time.

### Can I create a campaign through MCP?

No, that is not confirmed. Create with `firstsales campaigns create`, then use MCP to edit and watch if you wish.

### Can I let a coding agent run these commands?

Yes, with a narrow key and the CLI skill's rules. Keep write power away from any agent that reads replies, and never paste a key into the prompt.

### What do I do when the CLI says `unsupported_operation`?

Stop and report that the capability is not public. Signals and webhooks are in this group today, and the README says not to call app-private routes.

### How often should I rotate keys?

Every 90 days is a reasonable start, and sooner for keys that can write. Create the new key and test it with `whoami` before you revoke the old one.

### What should I leave in the app?

Judgment with context, rare setup, billing, people and permissions, anything unsupported, and the final approval of risky actions.

### How do I start if my team is new to this?

Follow the thirty-day plan: read-only checks, then reports, then gated writes, then launch discipline. Each week produces something useful on its own.

## Conclusion

The terminal does not make outbound smarter. It makes the routine part consistent, so people can spend their attention on the parts that need it.

Here are the takeaways.

- Run the boring half from the terminal: checks, reports, imports with gates, and previewed launches. Keep judgment in the app.
- Follow the loop every time: identity, read, smallest change, dry run, apply, verify.
- Use narrow keys, one per job, and never paste a key into a prompt or a post.
- Branch on exit codes, alert on auth failures, and stop on `unsupported_operation`.
- Run `campaigns readiness` and `campaigns send-test-email` before `campaigns start`, and keep delete commands manual because they cannot be undone.
- Create campaigns with `firstsales campaigns create`, not through MCP, and let any agent explain results without gaining write power.

Start tomorrow with `whoami`, `doctor` and the morning check. By the end of the month you can have a weekly report, a safe import and a launch checklist that anyone on the team can follow.

To go deeper, read the [daily outbound standup prompt](/blog/daily-outbound-standup-prompt), [weekly campaign reports with cron](/blog/weekly-campaign-report-cron-cli) and [API keys for outbound agents](/blog/api-keys-for-outbound-agents). To run outbound on a platform built for agent-safe automation, try [FirstSales](https://firstsales.io).