Skip to content

Commit b1e3529

Browse files
NOTICKET Add adjustable output format per subcommand (#3)
* Add adjustable output format option per subcommand Adds --format / -f flag to all 11 subcommands with choices: json (default), jsonl, csv, yaml, table. Nested objects are stringified for csv/table formats. Adds js-yaml dependency for YAML serialization. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix output format edge cases from code review - Validate --format input; exit with error on unknown value - Derive CSV/table headers from union of all row keys, not just rows[0] - Replace Math.max(...array) spread with reduce to avoid call stack limit - Fix CSV escaping: escape headers, handle \r, use \r\n line endings (RFC 4180) - Remove dead default parameter from print() since Commander always passes the value - Fix companies search: --industry no longer silently overwrites --query - Add comment clarifying YAML intentionally skips toRows() to preserve full structure - Add comment explaining FORMAT_OPTION tuple positional contract Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Document --format option in README and apollo-cli skill Adds a "Output formats" section covering json/jsonl/csv/yaml/table, when to use each, and the nested-response caveat for csv/table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1b532e6 commit b1e3529

8 files changed

Lines changed: 183 additions & 24 deletions

File tree

.claude/skills/apollo-cli/SKILL.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ version: 1.0.0
66

77
# Apollo CLI Skill
88

9-
Use the `apollo` CLI to search and enrich people and companies, find job postings, and surface news from Apollo.io. All output is JSON — pipe with `jq` to extract specific fields.
9+
Use the `apollo` CLI to search and enrich people and companies, find job postings, and surface news from Apollo.io. Output defaults to JSON for `jq` piping; use `-f, --format` to switch to `jsonl`, `csv`, `yaml`, or `table`.
1010

1111
## Authentication
1212

@@ -114,9 +114,29 @@ apollo news search --id <organization_id>
114114

115115
---
116116

117+
## Output formats
118+
119+
Every subcommand accepts `-f, --format <format>`:
120+
121+
| Format | When to use |
122+
|---|---|
123+
| `json` (default) | Pretty-printed JSON. Pipe to `jq` for field extraction. |
124+
| `jsonl` | One JSON object per line. Stream into log/data pipelines. |
125+
| `csv` | Flat CSV with headers; nested objects/arrays are stringified. Open in spreadsheets or feed to `mlr`/`csvkit`. |
126+
| `yaml` | Human-readable YAML. Good for inspecting deeply nested responses. |
127+
| `table` | ASCII bordered table. Good for terminal browsing of small responses. |
128+
129+
```bash
130+
apollo companies search --industry saas --format table
131+
apollo people bulk-enrich --emails a@b.com c@d.com --format jsonl
132+
apollo news search --company Stripe --format yaml
133+
```
134+
135+
For nested responses (search/list endpoints with pagination + items), prefer `json` + `jq` or `yaml`. `csv`/`table` will stringify nested structures into a single cell.
136+
117137
## Piping with jq
118138

119-
All commands output JSON. Use `jq` to extract specific fields:
139+
For default JSON output, use `jq` to extract specific fields:
120140

121141
```bash
122142
# Names and titles of VPs at Stripe

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# apollo-io-cli
22

3-
A command-line interface for the [Apollo.io API](https://apolloio.github.io/apollo-api-docs/). Search and enrich people and companies, find job postings, and surface news — all from your terminal, pipeable with `jq`.
3+
A command-line interface for the [Apollo.io API](https://apolloio.github.io/apollo-api-docs/). Search and enrich people and companies, find job postings, and surface news — all from your terminal, pipeable with `jq` and switchable to CSV / YAML / JSONL / table output.
44

55
<img width="1908" height="2294" alt="image" src="https://github.com/user-attachments/assets/eb7ebc43-1c64-466d-8959-e9c6e185433a" />
66

@@ -218,9 +218,34 @@ For agents not using Claude Code, see [`AGENTS.md`](./AGENTS.md) at the root for
218218

219219
---
220220

221+
## Output formats
222+
223+
Every subcommand accepts `-f, --format <format>` (default `json`):
224+
225+
| Format | Description |
226+
|---|---|
227+
| `json` | Pretty-printed JSON. Default. Pipe to `jq` for field extraction. |
228+
| `jsonl` | One JSON object per line. Useful for streaming into log/data pipelines. |
229+
| `csv` | Flat CSV with headers; nested objects/arrays are stringified into a single cell. |
230+
| `yaml` | Human-readable YAML. Easier to scan for deeply nested responses. |
231+
| `table` | ASCII bordered table. Good for quick terminal browsing of small responses. |
232+
233+
```bash
234+
apollo companies search --industry saas --format table
235+
apollo people bulk-enrich --emails a@b.com c@d.com --format jsonl
236+
apollo news search --company "Stripe" --format yaml
237+
apollo people search --title "CTO" --domain stripe.com --format csv > ctos.csv
238+
```
239+
240+
> Search/list responses include both pagination metadata and an array of records. `csv` and `table` formats render these as two columns with the array stringified — for analysis, prefer `json` + `jq` to project just the records:
241+
>
242+
> ```bash
243+
> apollo companies search --industry saas --format json | jq '.organizations[]' --compact-output > orgs.jsonl
244+
> ```
245+
221246
## Piping with jq
222247
223-
All commands output JSON to stdout, making them composable with `jq`:
248+
Default JSON output is composable with `jq`:
224249
225250
```bash
226251
# Get names and titles of VP Engineering at Stripe

bun.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"build:all": "bun build ./src/index.js --compile --target=bun-darwin-arm64 --outfile dist/apollo-macos-arm64 && bun build ./src/index.js --compile --target=bun-darwin-x64 --outfile dist/apollo-macos-x64 && bun build ./src/index.js --compile --target=bun-linux-x64 --outfile dist/apollo-linux-x64"
1313
},
1414
"dependencies": {
15-
"commander": "^12.1.0"
15+
"commander": "^12.1.0",
16+
"js-yaml": "^4.1.1"
1617
}
1718
}

src/commands/companies.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { apolloGet, apolloRequest } from '../api.js';
2-
import { print } from '../output.js';
2+
import { print, FORMAT_OPTION } from '../output.js';
33
import { parsePageOptions } from '../utils.js';
44

55
export function registerCompanies(program) {
@@ -20,14 +20,15 @@ export function registerCompanies(program) {
2020
.option('--hiring-for <titles...>', 'Currently hiring for job title(s)')
2121
.option('--per-page <n>', 'Results per page', '10')
2222
.option('--page <n>', 'Page number', '1')
23+
.option(...FORMAT_OPTION)
2324
.action(async (opts) => {
2425
const body = parsePageOptions(opts);
2526

26-
if (opts.query) body.q_organization_keyword_tags = [opts.query];
27+
if (opts.industry) body.q_organization_keyword_tags = opts.industry;
28+
else if (opts.query) body.q_organization_keyword_tags = [opts.query];
2729
if (opts.location) body.organization_locations = opts.location;
2830
if (opts.notLocation) body.organization_not_locations = opts.notLocation;
2931
if (opts.employees) body.organization_num_employees_ranges = [opts.employees];
30-
if (opts.industry) body.q_organization_keyword_tags = opts.industry;
3132
if (opts.technology) body.currently_using_any_of_technology_uids = opts.technology;
3233
if (opts.revenue) {
3334
const [min, max] = opts.revenue.split(',');
@@ -44,14 +45,15 @@ export function registerCompanies(program) {
4445
if (opts.hiringFor) body.q_organization_job_titles = opts.hiringFor;
4546

4647
const data = await apolloRequest('/mixed_companies/search', body);
47-
print(data);
48+
print(data, opts.format);
4849
});
4950

5051
companies
5152
.command('enrich')
5253
.description('Enrich a company profile')
5354
.option('--domain <domain>', 'Company domain (e.g. acme.com)')
5455
.option('--name <name>', 'Company name')
56+
.option(...FORMAT_OPTION)
5557
.action(async (opts) => {
5658
if (!opts.domain && !opts.name) {
5759
console.error('Error: provide --domain or --name');
@@ -75,7 +77,7 @@ export function registerCompanies(program) {
7577
process.exit(1);
7678
}
7779
const data = await apolloRequest('/organizations/enrich', { domain });
78-
print(data);
80+
print(data, opts.format);
7981
return;
8082
}
8183

@@ -84,30 +86,32 @@ export function registerCompanies(program) {
8486
if (opts.name) body.name = opts.name;
8587

8688
const data = await apolloRequest('/organizations/enrich', body);
87-
print(data);
89+
print(data, opts.format);
8890
});
8991

9092
companies
9193
.command('bulk-enrich')
9294
.description('Enrich multiple companies by domain')
9395
.requiredOption('--domains <domains...>', 'Company domains to enrich')
96+
.option(...FORMAT_OPTION)
9497
.action(async (opts) => {
9598
const body = { domains: opts.domains };
9699
const data = await apolloRequest('/organizations/bulk_enrich', body);
97-
print(data);
100+
print(data, opts.format);
98101
});
99102

100103
companies
101104
.command('get')
102105
.description('Get full details for a company by ID')
103106
.requiredOption('--id <id>', 'Apollo organization ID')
107+
.option(...FORMAT_OPTION)
104108
.action(async (opts) => {
105109
const data = await apolloRequest('/mixed_companies/search', {
106110
organization_ids: [opts.id],
107111
per_page: 1,
108112
page: 1,
109113
});
110-
print(data);
114+
print(data, opts.format);
111115
});
112116

113117
companies
@@ -116,9 +120,10 @@ export function registerCompanies(program) {
116120
.requiredOption('--id <id>', 'Apollo organization ID')
117121
.option('--per-page <n>', 'Results per page', '10')
118122
.option('--page <n>', 'Page number', '1')
123+
.option(...FORMAT_OPTION)
119124
.action(async (opts) => {
120125
const { page, per_page } = parsePageOptions(opts);
121126
const data = await apolloGet(`/organizations/${opts.id}/job_postings`, { page, per_page });
122-
print(data);
127+
print(data, opts.format);
123128
});
124129
}

src/commands/news.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { apolloRequest } from '../api.js';
2-
import { print } from '../output.js';
2+
import { print, FORMAT_OPTION } from '../output.js';
33
import { parsePageOptions } from '../utils.js';
44

55
export function registerNews(program) {
@@ -12,6 +12,7 @@ export function registerNews(program) {
1212
.option('--id <id>', 'Apollo organization ID to filter by')
1313
.option('--per-page <n>', 'Results per page', '10')
1414
.option('--page <n>', 'Page number', '1')
15+
.option(...FORMAT_OPTION)
1516
.action(async (opts) => {
1617
if (!opts.company && !opts.id) {
1718
console.error('Error: provide --company or --id');
@@ -39,6 +40,6 @@ export function registerNews(program) {
3940
page,
4041
per_page,
4142
});
42-
print(data);
43+
print(data, opts.format);
4344
});
4445
}

src/commands/people.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { apolloGet, apolloRequest } from '../api.js';
2-
import { print } from '../output.js';
2+
import { print, FORMAT_OPTION } from '../output.js';
33
import { parsePageOptions } from '../utils.js';
44

55
export function registerPeople(program) {
@@ -18,6 +18,7 @@ export function registerPeople(program) {
1818
.option('--industry <industries...>', 'Industry(s) to filter by')
1919
.option('--per-page <n>', 'Results per page', '10')
2020
.option('--page <n>', 'Page number', '1')
21+
.option(...FORMAT_OPTION)
2122
.action(async (opts) => {
2223
const body = parsePageOptions(opts);
2324

@@ -31,7 +32,7 @@ export function registerPeople(program) {
3132
if (opts.industry) body.organization_industry_tag_ids = opts.industry;
3233

3334
const data = await apolloRequest('/mixed_people/api_search', body);
34-
print(data);
35+
print(data, opts.format);
3536
});
3637

3738
people
@@ -43,6 +44,7 @@ export function registerPeople(program) {
4344
.option('--last-name <name>', 'Last name (use with --first-name and --company)')
4445
.option('--name <name>', 'Full name (use with --company)')
4546
.option('--company <domain>', 'Company domain (use with --name or --first-name/--last-name)')
47+
.option(...FORMAT_OPTION)
4648
.action(async (opts) => {
4749
if (!opts.email && !opts.linkedin && !opts.firstName && !opts.name) {
4850
console.error('Error: provide --email, --linkedin, or --name/--first-name with --company');
@@ -58,26 +60,28 @@ export function registerPeople(program) {
5860
if (opts.company) body.organization_name = opts.company;
5961

6062
const data = await apolloRequest('/people/match', body);
61-
print(data);
63+
print(data, opts.format);
6264
});
6365

6466
people
6567
.command('bulk-enrich')
6668
.description('Enrich multiple people by email')
6769
.requiredOption('--emails <emails...>', 'Email addresses to enrich')
70+
.option(...FORMAT_OPTION)
6871
.action(async (opts) => {
6972
const body = { details: opts.emails.map(email => ({ email })) };
7073
const data = await apolloRequest('/people/bulk_match', body);
71-
print(data);
74+
print(data, opts.format);
7275
});
7376

7477
people
7578
.command('email')
7679
.description('Get the email address for a person by Apollo person ID')
7780
.requiredOption('--id <id>', 'Apollo person ID')
81+
.option(...FORMAT_OPTION)
7882
.action(async (opts) => {
7983
const data = await apolloGet('/people/match', { id: opts.id });
80-
print(data);
84+
print(data, opts.format);
8185
});
8286

8387
people
@@ -88,6 +92,7 @@ export function registerPeople(program) {
8892
.option('--linkedin <url>', 'Company LinkedIn URL')
8993
.option('--per-page <n>', 'Results per page', '10')
9094
.option('--page <n>', 'Page number', '1')
95+
.option(...FORMAT_OPTION)
9196
.action(async (opts) => {
9297
const body = parsePageOptions(opts);
9398

@@ -96,6 +101,6 @@ export function registerPeople(program) {
96101
if (opts.linkedin) body.organization_linkedin_url = opts.linkedin;
97102

98103
const data = await apolloRequest('/mixed_people/api_search', body);
99-
print(data);
104+
print(data, opts.format);
100105
});
101106
}

0 commit comments

Comments
 (0)