Getting Started
discolike is the official command-line tool for the DiscoLike API. It exposes company discovery, enrichment, contact search, email finding, and account management as terminal commands that print JSON, so you can wire the API into shell scripts and cron jobs without writing Python.
The CLI ships as its own PyPI distribution, discolike-cli, and installs a single executable named discolike. It depends on the discolike SDK, which does the HTTP work and owns credential resolution.
Requirements
Section titled “Requirements”| Item | Value |
|---|---|
| Python | 3.10 or newer |
| Distribution | discolike-cli |
| Command installed | discolike |
| Dependencies | discolike==0.3.0 (pinned exactly, released together), typer>=0.12, rich>=13.0 |
| Current version | 0.3.0 |
Install
Section titled “Install”pip install discolike-cliIf you also want the SDK in the same environment, install it through the SDK’s cli extra instead; it pulls in discolike-cli alongside the library:
pip install "discolike[cli]"Install the CLI as a standalone tool, isolated from your project environments:
uv tool install discolike-cliuv tool install puts discolike on your PATH in its own virtual environment. To upgrade later:
uv tool upgrade discolike-cliRun the CLI without installing anything permanently:
uvx --from discolike-cli discolike --versionThe --from flag is required because the distribution name (discolike-cli) differs from the command name (discolike).
Confirm the install and see both version numbers at once:
discolike --version🪩 discolike-cli 0.3.0 (discolike 0.3.0)The first number is the CLI, the second is the SDK underneath it. They are released together and the CLI pins the SDK to the exact same version.
Global options
Section titled “Global options”These options belong to the root command and must appear before the subcommand:
discolike --api-key dk_... count --country US| Flag | Type | Default | Environment variable | Description |
|---|---|---|---|---|
--api-key | TEXT | none | DISCOLIKE_API_KEY | API key. Overrides the config file saved by discolike auth login. |
--base-url | TEXT | https://api.discolike.com/v1 | none | API base URL. |
--version | flag | none | none | Print CLI and SDK versions and exit. |
--install-completion | flag | none | none | Install completion for the current shell. |
--show-completion | flag | none | none | Show completion for the current shell, to copy it or customize the installation. |
--help | flag | none | none | Show help and exit. |
Every option in the CLI is long-form. There are no single-letter aliases anywhere, and DISCOLIKE_API_KEY is the only environment variable the CLI reads.
--base-url points the CLI at a different API host: a local test server, or a proxy. Leave it unset to use production.
Authentication
Section titled “Authentication”Every command needs a credential. On a workstation, log in once through the browser: discolike auth login opens the DiscoLike authorization page, you approve access, and the CLI saves the resulting OAuth session to a config file it refreshes on its own from then on. Where there is no browser, such as servers and CI, use an API key instead; the CLI resolves one from the command line, the environment, or the same config file.
-
Log in through the browser.
Terminal window discolike auth loginOpen this URL in your browser to log in:https://auth.discolike.com/oauth/2.1/authorize?response_type=code&client_id=...The CLI starts a temporary listener on
127.0.0.1, opens the URL, and waits up to 180 seconds for the browser to be redirected back. Approve the request and the terminal prints the success payload:{"logged_in": true, "method": "oauth", "expires_at": "2026-08-28T20:15:00+00:00"}The session is verified against the account usage endpoint before anything is written.
--no-browserprints the URL without opening a browser, and--portpins the loopback port so it can be forwarded over SSH; both are described under auth login.To use an API key instead, create one at app.discolike.com/account/management/keys and pass it with
--api-key, or pass--method api_keyto be prompted for it with hidden input:Terminal window discolike auth login --api-key dk_...Terminal window discolike auth login --method api_keyThe key is verified the same way. A bad key exits 3 and writes nothing to disk.
-
Confirm which credential is active.
Terminal window discolike auth status{"source": "config","method": "oauth","expires_at": "2026-08-28T20:15:00+00:00","expired": false,"valid": true}sourceisoptionfor a key passed as--api-key,envwhenDISCOLIKE_API_KEYis set, andconfigwhen the credential came from the config file.methodisoauthorapi_key. For an API key the payload carriesapi_key, masked to its last four characters ("…isco"; the leading…is JSON-escaped because the CLI emits ASCII-safe JSON), instead ofexpires_atandexpired.expiredonly says whether the access token has passed its expiry; the session is refreshed on the next command regardless, so"expired": truenext to"valid": trueis normal. -
Remove the saved credential when you are done.
Terminal window discolike auth logout{"logged_out": true}Logging out deletes the session or key but keeps the CLI’s OAuth client registration, so the next login skips the browser consent screen. It is safe to run when nothing is saved.
Browser consent and re-registration
Section titled “Browser consent and re-registration”On the first browser login the CLI registers itself with the authorization server as a public OAuth client (PKCE, no client secret) and saves the registration as oauth_client in the config file. The authorization server remembers your consent per client, so reusing that registration is what lets later logins go straight from the browser back to the terminal without asking again. A registration is reused when it was issued by the same authorization server as the --base-url in use and its loopback port can be bound again.
The CLI registers a fresh client, and the browser asks for consent once more, when:
- you log in against a different server with
--base-url, since registrations are per server; - the port recorded in the saved registration is busy, or you pass a
--portthat differs from it. The server matches the redirect URI literally, port included, so a registration cannot be reused on another port; - the authorization server no longer recognises the saved client (
invalid_clientorunauthorized_client). The CLI discards the registration, registers a fresh one, and runs the browser flow once more. If that fresh client is rejected too, the login fails with aLoginError.
Credential precedence
Section titled “Credential precedence”The SDK resolves the credential on every command and uses the first source that yields one:
| Order | Source | How to set it |
|---|---|---|
| 1 | The --api-key global flag | discolike --api-key dk_... count --country US |
| 2 | The DISCOLIKE_API_KEY environment variable | export DISCOLIKE_API_KEY=dk_... |
| 3 | The config file written by auth login, holding an OAuth session or an API key | discolike auth login |
An API key from the flag or the environment always wins, even when an OAuth session is saved. If none of the three yields a credential, the command fails before any request is sent:
No API key found. Set the DISCOLIKE_API_KEY environment variable, pass api_key=...,or run `discolike auth login`. Create a key at https://app.discolike.com/account/management/keysThat is an AuthenticationError, so the exit code is 3.
Both auth login and auth status honour --base-url: auth login logs in to that host’s authorization server, and auth status verifies against it. An explicit --api-key on the command line switches auth login to the API-key flow with that key; an ambient DISCOLIKE_API_KEY does not, so the browser flow still runs in an environment that happens to have a key set.
The config file
Section titled “The config file”| Item | Value |
|---|---|
| Path | ${XDG_CONFIG_HOME:-~/.config}/discolike/config.json |
| Contents | One of the two shapes below, selected by auth_method |
| Permissions | 0600, owner read/write only |
{"auth_method": "api_key", "api_key": "dk_..."}{ "auth_method": "oauth", "oauth": { "access_token": "eyJ...", "refresh_token": "...", "expires_at": 1756426800.0, "client_id": "...", "token_endpoint": "https://auth.discolike.com/oauth/2.1/token" }, "oauth_client": { "client_id": "...", "redirect_uri": "http://127.0.0.1:51234/callback", "issuer": "https://auth.discolike.com/oauth/2.1" }}oauth is the session the SDK sends and refreshes; a refresh rewrites it with the rotated tokens. oauth_client is the CLI’s client registration, kept across auth logout and carried over when you log in with an API key, so it is present in either shape once you have logged in through the browser.
The file is created with mode 0600 and re-chmodded after each write, so it is never group- or world-readable. A missing, unreadable, or malformed config file, or an oauth section with a missing field, is treated as “no credential found” rather than an error, which means a corrupt file degrades to the environment variable or to the guidance message above.
For CI and containers, prefer the environment variable over the config file: nothing has to be written to the image, and auth status will report source: env.
Key creation, rotation, and revocation are covered in API access.
First call
Section titled “First call”With a key in place, the cheapest useful command is a count, which returns a single number and bills nothing per record:
discolike count --country US --category software{ "count": 68900}From here:
- Command Reference: every command and option, grouped the way the CLI groups them.
- Output and scripting:
--format, exit codes, stderr, and piping results intojq. - Common workflows: end-to-end recipes that chain several commands.
- Python SDK: the same API as a library, with typed models.
Output and scripting
Section titled “Output and scripting”Every DiscoLike command prints machine-readable JSON by default when its output is not a terminal, writes errors to stderr as a single JSON object, and signals the error class through the exit code. That combination is what makes the CLI usable from a shell script without parsing prose.
Output formats
Section titled “Output formats”There are two formats: pretty-printed JSON and a Rich-rendered table. --format selects between them:
| Value | Result |
|---|---|
table | Render a table if the payload qualifies; otherwise JSON |
json | JSON |
| anything else | JSON |
| omitted | Table if stdout is a TTY and the payload qualifies; otherwise JSON |
--format is free text, and only the exact string table changes the output; every other value, --format json included, produces JSON. For CSV, use append --csv --output FILE, which writes the API’s CSV bytes to the file verbatim.
TTY auto-selection
Section titled “TTY auto-selection”When you omit --format, the CLI checks whether stdout is a terminal. Interactively you get a table; the moment you pipe or redirect, you get JSON. Nothing else about the command changes, so the same invocation is safe to develop interactively and then drop into a script.
When a table is possible
Section titled “When a table is possible”A table is only rendered when the payload is a non-empty list of objects with at least one column that is scalar in every row. Most DiscoLike endpoints return an object with the list nested under a key (count returns {"count": ...}, queries list returns {"results": [...]}, search-providers list returns {"providers": [...]}), and an object is never tabular. Those commands print JSON no matter what you pass:
discolike count --country US --category software --format table{ "count": 68900}discover is the main command that returns a bare list, so it is the one where --format table and the interactive default actually produce a table. Even there the table is partial: nested fields such as address, keywords, and business_model are not scalar in every row, so they are dropped, and only the first 8 surviving columns are shown.
When a table is rendered:
| Rule | Behavior |
|---|---|
| Column selection | A key becomes a column only if its value is a string, number, boolean, or null in every row. One nested value anywhere drops the whole column. |
| Column limit | The first 8 qualifying columns |
| Cell truncation | Values longer than 80 characters are cut and suffixed with … |
| Null cells | Rendered as an empty cell |
Because columns are dropped rather than flattened, the table is a preview, not a complete view of the record. Use JSON whenever you need the full payload.
stdout and stderr
Section titled “stdout and stderr”| Stream | Carries |
|---|---|
| stdout | Command results, either JSON or the rendered table |
| stderr | Error payloads, --wait progress lines, and the auth login success payload |
Errors are written as a single line of JSON to stderr:
{"error": "AuthenticationError", "message": "Invalid API Key or Session", "status_code": 401}| Field | Type | Notes |
|---|---|---|
error | string | The exception class name |
message | string | The server’s message, extracted from the response body |
status_code | integer or null | The HTTP status, or null when the failure never reached the server |
retry_after | number | Present only on a RateLimitError, and only when the response carried a numeric Retry-After header |
Nothing is written to stdout when a command fails, so cmd > out.json leaves an empty file rather than a half-written one:
discolike --api-key dk-bogus count --country US 2>/dev/null; echo "exit=$?"exit=3Progress from --wait also goes to stderr, one line per poll:
progress: 40%That keeps stdout a clean JSON document even for long-running jobs.
Exit codes
Section titled “Exit codes”| Exit code | Condition | HTTP status |
|---|---|---|
| 0 | Success | none |
| 1 | Server error, or any unmapped failure | 5xx, or any other status |
| 1 | Browser login failure (LoginError): timeout, denied consent, or an invalid callback | none |
| 2 | Validation error (client-side or server-side), or a command-line usage error | 400, 422, or none |
| 3 | Authentication error | 401 |
| 3 | Plan access error | 402, 403 |
| 4 | Rate limited | 429 |
| 5 | Network or connection failure | none |
| 6 | Not found | 404 |
Exit code 2 covers three cases: a request the server rejected as invalid, a request the CLI rejected before sending anything, and a command line the CLI could not parse.
Every option and --param value is validated against the API’s own schema before the request is built, so an unknown enum value, a number outside its range, or a missing required option fails locally with the same JSON shape as a server error and "status_code": null:
discolike discover --icp-prompt "B2B payments" --max-records 1; echo "exit=$?"{"error": "ValidationError", "message": "max_records: Input should be greater than or equal to 5", "status_code": null}exit=2Bad flags, an unknown command, and a malformed --param are usage errors:
discolike count --param bogus; echo "exit=$?"Invalid value: --param must be in KEY=VALUE form, got 'bogus'exit=2Usage errors are rendered by the CLI framework as a bordered panel on stderr, not as a single-line JSON object. Distinguish the two by parsing: if stderr is valid JSON, it was a validation or API error (status_code is null when the request never left the machine); if not, it was a usage error.
Scripting
Section titled “Scripting”Piping to jq
Section titled “Piping to jq”Because non-TTY output is always JSON, no flag is needed:
discolike count --country US --category software | jq -r '.count'68900Field extraction from a list-returning command works the same way:
discolike discover --icp-prompt "B2B payments infrastructure" --max-records 25 \ | jq -r '.[].domain'Checking exit codes
Section titled “Checking exit codes”Branch on the class of failure rather than on the message text:
#!/usr/bin/env bashset -uo pipefail
if out=$(discolike count --country US --category software 2>err.json); then echo "count: $(jq -r '.count' <<<"$out")"else case $? in 3) echo "auth or plan problem: $(jq -r .message err.json)" >&2; exit 1 ;; 4) sleep "$(jq -r '.retry_after // 60' err.json)"; exec "$0" ;; 5) echo "network failure, retrying later" >&2; exit 75 ;; *) echo "failed: $(jq -r .message err.json)" >&2; exit 1 ;; esacfiNote the 2>err.json: the error JSON is on stderr, so it has to be captured separately from the result.
Async jobs: --wait versus polling
Section titled “Async jobs: --wait versus polling”Seven commands start an async job and take --wait and --timeout: match (with --file), validate-icp, segment, contacts bulk-match, contacts generate, discogen run, and discogen run-personas.
Without --wait, the command returns immediately with the task handle and a ready-made polling command:
{ "task_id": "0f2c...", "task_family": "bulkmatch", "hint": "poll with: discolike discogen status 0f2c... --family bulkmatch"}Capture the id and poll on your own schedule:
task_id=$(discolike match --file names.csv | jq -r '.task_id')until discolike discogen status "$task_id" --family bulkmatch \ | jq -e '.status | IN("completed", "failed", "cancelled")' >/dev/null; do sleep 30doneWith --wait, the command blocks, streams progress: N% lines to stderr, and prints the finished results to stdout. --timeout caps the wait in seconds and defaults to 900.0:
discolike match --file names.csv --wait --timeout 1800 > matched.jsonChoose --wait for a job that fits inside one process: a cron entry, a CI step. Choose the polling form when the job may outlive the shell, or when you want to start several jobs and collect them later.
discogen status and discogen cancel work for any task family, not just DiscoGen. Pass the --family value the job handed you. --family accepts discogen, bulkmatch, contactmatch, or segment, and defaults to discogen. The terminal statuses are completed, failed, and cancelled.
The --param escape hatch
Section titled “The --param escape hatch”discover, count, contacts search, contacts count, and contacts discover accept --param KEY=VALUE, which forwards an arbitrary parameter to the API. Use it for a query parameter the CLI has no flag for, so a new API capability does not require a CLI upgrade.
discolike discover --icp-prompt "B2B payments" --param min_similarity=200Rules:
| Rule | Behavior |
|---|---|
| Splitting | Split on the first =. The value may contain further = characters. |
| Lists | A value containing a comma becomes a list. --param social=linkedin,youtube sends social=linkedin&social=youtube. |
| Repeatable | Pass --param more than once for multiple parameters. |
| Precedence | A named flag overrides --param for the same key. |
| Malformed | A value with no = raises a usage error and exits 2. |
| Unknown key | A key the CLI does not know is forwarded to the API unchanged, so a new API parameter works without a CLI upgrade. |
| Known key, bad value | A value outside the API schema (unknown enum value, number out of range) is rejected before the request and exits 2. |
Comma splitting is unconditional: a value containing a comma is always sent as a list. Use a named flag when you need to send a single value that contains a comma.
Precedence in practice: the named flag wins and the --param value is discarded:
discolike count --country US --param country=DE --category software{ "count": 68900}That is the US count, not the German one; --country US won.
A bad value for a known key fails fast rather than reaching the server:
discolike count --country US --param social=github{"error": "ValidationError", "message": "social.0: Input should be 'facebook', 'instagram', 'linkedin', 'pinterest', 'threads', 'tiktok', 'twitter', 'x', 'yelp', 'youtube', 'googleplay', 'applestore', 'amazon', 'vk', 'bluesky' or 'xing'", "status_code": null}The parameter names accepted by each endpoint are listed in the API reference.
Common workflows
Section titled “Common workflows”Task-shaped workflows that chain several commands together. Each recipe states the goal, the commands, the real output, and what to do next. For the full option list on any command, see Command Reference.
Every example assumes DISCOLIKE_API_KEY is set or discolike auth login has been run; see Authentication.
Size a market before you spend credits
Section titled “Size a market before you spend credits”count is free of record charges and takes the same filters as discover. Iterate on count until the number looks like a segment you would actually work, then run discover once.
-
Start broad and see how big the universe is.
Terminal window discolike count --category SAAS --country US{"count": 248818} -
Add a size band.
--employee-rangetakesmin,max, with a comma, not a dash.Terminal window discolike count --category SAAS --country US --employee-range 51,200{"count": 9575} -
Add an intent signal.
--phrase-matchrequires the phrase to appear on the company website.Terminal window discolike count --category SAAS --country US --employee-range 51,200 \--phrase-match "SOC 2"{"count": 977} -
Or filter on what they already run.
--tech-stacktakes the vendor’s domain, not a product name:hubspot.com, nothubspot.Terminal window discolike count --category SAAS --country US --employee-range 51,200 \--tech-stack hubspot.com{"count": 5097}
A bare product name is rejected with exit code 2:
discolike count --category SAAS --tech-stack hubspot{"error": "ValidationError", "message": "Invalid Domain : hubspot", "status_code": 400}Once the count is in a range you can act on, reuse the exact same flags on discover and add --max-records. See Count for the full filter vocabulary.
Build a target list from an ICP description and write it to CSV
Section titled “Build a target list from an ICP description and write it to CSV”--icp-prompt takes plain language and derives filters from it. Combine it with hard filters to keep the result grounded.
discolike discover \ --icp-prompt "developer tools startups selling to engineering teams" \ --country US \ --max-records 5 \ > targets.jsondiscover writes a top-level JSON array to stdout, one object per company:
jq -r '.[] | [.domain, .name, .employees, .similarity] | @tsv' targets.jsoncodecones.com CodeCones, Inc. 1-10 91.0linearb.io LinearB 51-200 91.0aionyxtech.com Aionyx Technologies Inc. 1-10 91.0digitaljackalope.com Digital Jackalope LLC 1-10 91.0ibute.tech ibute Technologies 1-10 90.0Turn it into a CSV your CRM will accept:
jq -r '(["domain","name","employees"]), (.[] | [.domain, .name, .employees]) | @csv' \ targets.json > targets.csv"domain","name","employees""codecones.com","CodeCones, Inc.","1-10""linearb.io","LinearB","51-200""aionyxtech.com","Aionyx Technologies Inc.","1-10""digitaljackalope.com","Digital Jackalope LLC","1-10""ibute.tech","ibute Technologies","1-10"Each result carries similarity, score, revenue_range, vendors, and keywords. Pull whatever your downstream system needs out of the same JSON rather than making a second call.
Next: feed targets.csv into enrichment or contact discovery.
Enrich an existing CSV of domains
Section titled “Enrich an existing CSV of domains”append is synchronous and returns the enriched rows in the same call. Point it at a CSV, name the column that holds the domains, and pick one or more datasets.
cat seed.csvdomainstripe.comlinear.appdiscolike append seed.csv --dataset bizdata --domain-column domainReturns a JSON array, one object per input row. Each row keeps input:domain so you can join back to the source file:
[ { "domain": "stripe.com", "input:domain": "stripe.com", "name": "Stripe", "status": { "status": "active", "confidence": 0.89 }, "score": 701, "start_date": "2011-01-04", "address": { "street": "354 Oyster Point Blvd", "city": "South San Francisco", "state": "CA", "zip": "94080", "country": "US" }, "phones": ["+16504279276", "+18889262289"], "public_emails": ["sales@stripe.com", "..."], "..." }, "..."]To get flat CSV back instead of nested JSON, pass --csv and --output:
discolike append seed.csv --dataset bizdata --domain-column domain \ --csv --output enriched.csv{ "written": "enriched.csv", "bytes": 3530}The output columns are namespaced by dataset:
input:domain,bizdata:social_urls,bizdata:redirect_domain,bizdata:revenue_range,bizdata:confidence,...Repeat --dataset to append several at once. See Append for the dataset catalogue.
Match a messy CRM export of company names to domains
Section titled “Match a messy CRM export of company names to domains”For a single name, match is synchronous. Use the address flags to disambiguate.
discolike match "Stripe" --country US{ "query": { "name": "Stripe", "country": "US", "state": null, "city": null, "zip": null, "phones": null }, "matches": [ { "domain": "stripe.com", "name": "Stripe", "status": { "status": "active", "confidence": 0.89 }, "score": 701, "address": { "city": "South San Francisco", "state": "CA", "country": "US" }, "..." } ]}Pull just the winner:
discolike match "Stripe" --country US | jq -r '.matches[0] | {domain, name, score}'{ "domain": "stripe.com", "name": "Stripe", "score": 701}For a whole CRM export, swap the positional name for --file. That switches match to an async bulk job:
discolike match --file crm-accounts.csv --name-column company_name --wait --timeout 1800--wait streams progress: N% lines to stderr and prints the results to stdout when the job finishes, so > out.json captures only the payload.
Without --wait you get the handle immediately and can walk away:
discolike match --file crm-accounts.csv --name-column company_name{ "task_id": "...", "task_family": "bulkmatch", "hint": "poll with: discolike discogen status {id} --family {family}"}--name-column defaults to name. --strict / --no-strict and --local-mode / --no-local-mode are tri-state: leave them off entirely and the server default applies.
The Python equivalent is client.match.bulk(MatchBulkParams(name_column=...), file=...), which returns a Job you can .wait() on; see match and Job.
Find contacts at a set of companies, then get their emails
Section titled “Find contacts at a set of companies, then get their emails”Size the contact pool first; contacts count is the cheap probe.
discolike contacts count --domain stripe.com --seniority vp --has-email{ "count": 56}Then pull the records. --has-email restricts results to contacts that already carry a verified address.
discolike contacts search --domain linearb.io --seniority vp --has-email --max-records 20 \ > contacts.json
jq -r '.[] | [.name, .title, .email, .email_validated] | @tsv' contacts.jsonLior Shlezinger VP Finance lior@linearb.io trueJessica Miller VP of People & Culture jess@linearb.io trueIlan Rado Vice President of Product Management ilan.rado@linearb.io trueCraig Zelley VP, Revenue Operations craig.zelley@linearb.io trueRepeat --domain to cover a whole account list. To drive it from a file of domains:
xargs -a domains.txt -I{} discolike contacts search --domain {} --has-email --max-records 20 \ > contacts.ndjsonWhen the index has no email for a person, find one with discolike email. The batch finder reads a CSV with exactly three columns:
first_name,last_name,domainPatrick,Collison,stripe.comAda,Lovelace,acme.comdiscolike email find-batch --contacts-file people.csv --wait --timeout 900 > emails.jsonA batch holds at most 500 contacts, so split longer lists. Mix in one-off people with repeatable --contact "First,Last,domain.com" flags. Only proven addresses bill; catch-all domains and pattern guesses come back free.
For a single person, skip the CSV:
discolike email find Patrick Collison stripe.com --wait --timeout 120{ "first_name": "patrick", "last_name": "collison", "domain": "stripe.com", "status": "catch_all_pattern", "result": { "email": "patrick.collison@stripe.com", "pattern": "first.last", "tier": 1, "smtp_code": 0, "valid": false }, "is_catch_all": true, "mx_host": "aspmx3.googlemail.com", "provider": "Everything Else", "attempts": 0, "duration_ms": 1473, "error": null}Drop --wait on either command to get an ID back immediately and poll later with discolike email job <job_id> or discolike email results <batch_id>. See email commands, email, and the Email Find endpoint.
Run a DiscoGen research job and poll it to completion
Section titled “Run a DiscoGen research job and poll it to completion”DiscoGen answers a free-text research question across a set of domains. It is always an async job.
Fire and forget, then poll on your own schedule:
discolike discogen run \ --query "Which of these companies has publicly announced a SOC 2 Type II certification?" \ --domain stripe.com --domain linear.app --domain vercel.com{ "task_id": "...", "task_family": "discogen", "hint": "poll with: discolike discogen status {id} --family {family}"}discolike discogen status <task_id>discolike discogen cancel <task_id>Or block in one call:
discolike discogen run \ --query "Which of these companies has publicly announced a SOC 2 Type II certification?" \ --domain stripe.com --domain linear.app \ --wait --timeout 1800--timeout defaults to 900.0 seconds. A client-side timeout does not cancel the job; it keeps running server-side, and you can pick it back up with discogen status.
discogen status and discogen cancel are the polling front-end for every async family, not just DiscoGen. Pass --family to reach the others:
| Started by | --family |
|---|---|
discolike discogen run, discogen run-personas, validate-icp, contacts generate | discogen (default) |
discolike match --file | bulkmatch |
discolike contacts bulk-match | contactmatch |
discolike segment | segment |
discolike discogen status <task_id> --family bulkmatchAn unknown family exits 2. See DiscoGen for the research parameters and Job for the Python equivalent.
Stop re-surfacing companies you already own
Section titled “Stop re-surfacing companies you already own”Save your existing customers as an exclusion list once, then attach it to every discover run.
--domain is repeatable, so build the argument list from a file rather than typing it out:
args=()while read -r d; do args+=(--domain "$d"); done < customers.txt
discolike queries create-exclusion-list \ --name "Existing customers" \ "${args[@]}" \ --tag customers{ "query_id": "a20af20c-0e3b-4dbf-bf08-2ade89993fc6", "query_name": "Existing customers", "action": null, "domain_count": 20, "persona_id_count": 0, "row_count": null, "tags": ["customers"], "message": null}Now pass the id to discover. The same query, before and after:
discolike discover --icp-prompt "developer tools startups selling to engineering teams" \ --country US --max-records 5codecones.com CodeCones, Inc.linearb.io LinearBaionyxtech.com Aionyx Technologies Inc.digitaljackalope.com Digital Jackalope LLCibute.tech ibute Technologiesdiscolike discover --icp-prompt "developer tools startups selling to engineering teams" \ --country US --max-records 5 \ --exclusion-query-id a20af20c-0e3b-4dbf-bf08-2ade89993fc6codecones.com CodeCones, Inc.aionyxtech.com Aionyx Technologies Inc.digitaljackalope.com Digital Jackalope LLCibute.tech ibute Technologiesallstacks.com Allstacks, Inc.linearb.io is gone and the slot is backfilled. --exclusion-query-id is repeatable, so you can stack a customers list, a churned-accounts list, and an open-opportunities list on one call. For ad-hoc one-offs use --exclude-domain instead, which is also repeatable.
Keep the list current by updating it as you close deals, and find it later by tag:
discolike queries list --tag customersdiscolike queries update <query_id> --name "Existing customers (2026)" --tag customersSave a round of results and reuse it
Section titled “Save a round of results and reuse it”Persist the domains you already surfaced so the next run can exclude them, or so a teammate can pick up the same set.
discolike discover --icp-prompt "developer tools startups selling to engineering teams" \ --country US --max-records 5 \ | jq -r '(["domain","name"]), (.[] | [.domain, .name]) | @csv' > round1.csv
discolike queries save-results \ --input round1.csv \ --name "Round 1 — devtools US" \ --action discover \ --domain-column domain \ --tag round1{ "query_id": "a848a58a-d0f3-4f5a-9003-768de7460e89", "query_name": "Round 1 — devtools US", "action": "thin_discover", "domain_count": 5, "persona_id_count": 0, "row_count": 5, "tags": ["round1"], "message": null}--input accepts .csv (header row, every value stays a string) or .json (a list of row objects); the extension decides. A missing file or malformed JSON exits 2.
Unlike create-exclusion-list, save-results has no 20-domain minimum; five rows saved fine above. Both produce a query_id usable as --exclusion-query-id, so the next round skips what you already pulled:
discolike discover --icp-prompt "..." --country US --max-records 5 \ --exclusion-query-id a848a58a-d0f3-4f5a-9003-768de7460e89Clean up when a list is stale:
discolike queries delete a848a58a-d0f3-4f5a-9003-768de7460e89{ "deleted": "a848a58a-d0f3-4f5a-9003-768de7460e89"}Wire the CLI into a shell script
Section titled “Wire the CLI into a shell script”Three properties make the CLI scriptable: results go to stdout, errors and progress go to stderr, and the exit code tells you which failure you hit.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Server error, any unmapped failure, or a failed browser login (LoginError) |
| 2 | Validation error, client-side (bad enum value, number out of range, missing required option) or server-side (400/422), or a usage error: bad flag, unknown command |
| 3 | Authentication (401) or plan access (402/403) |
| 4 | Rate limited (429) |
| 5 | Connection failure |
| 6 | Not found (404) |
Verified:
discolike count --category SAAS >/dev/null 2>&1; echo $? # 0discolike count --nope >/dev/null 2>&1; echo $? # 2discolike --api-key bogus account usage >/dev/null 2>&1; echo $? # 3discolike discogen status not-a-real-task >/dev/null 2>&1; echo $? # 6Errors are a single line of JSON on stderr, never mixed into stdout:
{"error": "AuthenticationError", "message": "Invalid API Key or Session", "status_code": 401}A RateLimitError adds retry_after when the server sent a numeric Retry-After header, which is the hook for a backoff loop.
A script that branches on the exit code
Section titled “A script that branches on the exit code”#!/usr/bin/env bashset -uo pipefail
code=0out=$(discolike count --category SAAS --country US --employee-range 51,200 2>err.json) || code=$?
case $code in 0) ;; 3) echo "auth problem: $(jq -r .message err.json)" >&2; exit 1 ;; 4) echo "rate limited, retry in $(jq -r '.retry_after // 60' err.json)s" >&2; exit 1 ;; *) echo "count failed ($code): $(jq -r .message err.json)" >&2; exit "$code" ;;esac
count=$(jq -r .count <<<"$out")if (( count > 50000 )); then echo "segment too broad ($count) — tighten the filters" >&2 exit 1fi
discolike discover --category SAAS --country US --employee-range 51,200 \ --max-records 100 > targets.jsonFormat is TTY-dependent
Section titled “Format is TTY-dependent”--format is only honoured as the literal string table. Everything else, including --format json, produces JSON. With no --format, the CLI emits a rich table when stdout is a terminal and JSON when it is a pipe or a file. In a script you therefore get JSON automatically, but pass --format json anyway if the script might ever run attached to a TTY. See Output and scripting for the table rules.
Resuming a long job by task id
Section titled “Resuming a long job by task id”Start the job, keep the id, and poll from anywhere:
task_id=$(discolike match --file crm-accounts.csv --name-column company_name \ | jq -r .task_id)echo "$task_id" > .last-task
while true; do state=$(discolike discogen status "$task_id" --family bulkmatch | jq -r .status) echo "state: $state" >&2 case "$state" in completed|failed|cancelled) break ;; esac sleep 30done
[ "$state" = "completed" ] || { echo "job $task_id ended as $state" >&2; exit 1; }discolike discogen status "$task_id" --family bulkmatch > results.jsonA status response carries status and a progress percentage. The three terminal states are completed, failed, and cancelled; break on all three or the loop never exits.
The task id stays valid independently of the process that started it, so a --wait that timed out client-side is not lost. Re-poll with discogen status and the correct --family.
When to reach for the SDK instead
Section titled “When to reach for the SDK instead”The CLI and the Python SDK cover the same API. Reach for Python when you need:
| Task | In Python |
|---|---|
| Fanning out across hundreds of domains | AsyncDiscolike runs requests concurrently; see Async. |
| Custom retry or backoff policy | Configure max_retries and timeout on the client; see The client. |
| Joining several jobs together | Job.wait() and typed result models, instead of parsing JSON in bash; see Job. |
Start at Getting Started for the library equivalent of every command on this page.
Shell completion
Section titled “Shell completion”Typer’s built-in completion support is enabled. Install it for the shell you are running:
discolike --install-completionTo inspect the script first, or to install it somewhere of your own choosing:
discolike --show-completionBoth flags detect the current shell from the environment and fail with Shell not supported. if they cannot identify it. Run them from an interactive shell rather than from a script. Completion applies to the root command and its subcommands; there are no per-group completion flags.