ethos-cli
v0.24.0
Published
Commander-based CLI for Cluster table operations
Readme
Cluster CLI
Commander-based CLI for Cluster agent and table operations.
Native human calling
Cluster Dialer can use the computer's microphone and speakers without an open
dashboard. Install the Cluster Audio companion
on the computer with the microphone, then authenticate the CLI as the calling
human and workspace. Run ethos dialer audio-devices to select devices and
ethos dialer microphone --consent to connect them. Keep that terminal open;
use its returned session ID with ethos dialer call in another terminal, or
let a connected agent place the call through Cluster MCP. Ctrl+C disconnects
audio and requests hangup. OS microphone permission is required. For a remote
development stack, run capture on the human's computer and point its CLI at
the test backend; running capture on the server cannot access a laptop mic.
Install
npm install -g ethos-cliAuth
The CLI can connect through your existing browser session:
ethos auth login
ethos auth status
ethos auth logoutUse --app-url if your frontend is not on the default local/prod URL:
ethos --api-url http://127.0.0.1:8001 auth login --app-url http://localhost:3001When running from Codex, pass the current thread UUID to return to that thread after browser approval:
ethos auth login --codex-thread-id "$CODEX_THREAD_ID"You can also configure auth with flags or environment variables:
export ETHOS_API_URL="https://api.ethos.hello-cluster.com"
export ETHOS_API_KEY="..."
export ETHOS_ORG_ID="..."Every command also accepts --api-url, --api-key, --access-token, --org-id, and --json.
Connected inboxes
ethos accounts email --jsonLists all email accounts connected to the active workspace, with their email addresses, account IDs, connection statuses, and email settings. Includes inboxes not yet assigned to campaigns.
Exclusions
ethos exclusions list --kind domain --search acme
ethos exclusions add --kind email [email protected] [email protected]
ethos exclusions add --kind linkedin --file profiles.txt
ethos exclusions remove <exclusion-id> <exclusion-id>Manages the workspace exclusion list: email addresses, website domains, and
LinkedIn profiles that no campaign will contact. A domain excludes everyone at
that company. add takes one --kind per call, from arguments, a --file with
one value per line and no header row, or both; invalid values are skipped and
reported. list pages with --limit and --offset, and remove takes the
exclusion_id values that list returns. On a draft campaign,
ethos campaigns get reports which staged leads launch would leave out in
list_contact_exclusions.
Managed inboxes
ethos inboxes list
ethos inboxes domains
ethos inboxes check-domains acmemail --tlds com,net
ethos inboxes pre-warmed --contains mail
ethos inboxes quote --file order.json
ethos inboxes order --file order.json --yes
ethos inboxes order-status <order-id> --wait 120
ethos inboxes warmup <mailbox-id> --onTo check exact domain variations in bulk, supply a text file with one domain per line (up to 50):
ethos inboxes check-domains-bulk --file domains.txt
printf 'getcluster.com\ntrycluster.co\n' | ethos inboxes check-domains-bulk --file -Blank lines are ignored; the server lowercases and deduplicates domains. Each result reports
available, unavailable, restricted, invalid, workspace_conflict, or error, with the yearly
credit price when available. Errors are inconclusive and can be retried. The check does not purchase anything.
Buys and manages managed inboxes and the managed domains they send from, paid
for with workspace credits. order without --yes only prints the quote and
places nothing; credits are spent only with --yes. order-status --wait
blocks for up to 300 seconds until the order finishes. The order file looks like:
{
"domains": [{ "domain": "acmemail.com", "years": 1 }],
"mailboxes": {
"acmemail.com": [
{ "username": "alice", "first_name": "Alice", "last_name": "Example", "on_warmup": true }
]
}
}Leave domains empty to add inboxes to one domain the workspace already owns.
Quotes include a setup expectation: new inboxes are expected to take around 30 minutes, but can take longer.
Sender connection can take hours, and completing setup does not mean warm-up is complete. Pre-warmed orders
have separate delivery guidance. Order responses include setup progress separately from purchase status:
a succeeded purchase can still have inboxes being created. inboxes list keeps those orders visible until
the inboxes arrive. After one bounded status check, use setup.check_after to decide when to run
ethos inboxes order-status <order-id> again. Setup continues after the command exits; do not place the order again.
When list reports registrant_needed: true, the workspace's first order must
also carry a registrant object: email, first_name, last_name, company,
address_line_one, address_line_two, city, state, country (2-letter
code), postal_code, phone_cc, and phone.
Skills
Cluster workflow guidance is served as resources by the hosted MCP server. Connect
an AI client directly to https://api.ethos.hello-cluster.com/mcp using streamable
HTTP and OAuth, then approve access in the browser. No plugin is required; the
Cluster plugin is retired. Start a fresh client session after connecting and
verify the active workspace with the read-only get_workspace_overview tool.
The CLI remains supported independently through ethos auth login. It no longer
bundles or installs skills. If a previous CLI version installed skills on this
machine, inspect and clean them up with:
ethos skills list
ethos skills removeAgent Commands
ethos agents list
ethos agents get <agent-id>
ethos agents create \
--name "Company enricher" \
--workflow-type company_enrichment \
--ability web-search \
--ability linkedin-company-lookup \
--tool-preset browser
ethos agents configure <agent-id> --workflow-type people_sourcing --clear-abilities
ethos agents delete <agent-id>Workflow/table compatibility is shown in --help: company tables support company enrichment columns and people sourcing, people tables support person enrichment columns. The CLI sends the configured request and lets backend validation decide the final result.
Org-scoped custom signals appear in ethos signals list with keys such as custom:<assignment-id> and configuration_mode: "managed". Use those keys with ethos signals pull and workflow commands exactly like built-in signals. Use the resulting sourcing job ID with ethos signals pull-status. Omit --config for managed custom signals; Cluster applies the organization’s current configuration revision server-side.
Organization members can manage their active organization’s complete assignment lifecycle through ethos custom-signals list|create|update|apply|preview|preview-status. The backend scopes every command to the active organization; apply atomically moves its active workflows to the current managed revision.
For a one-time pull of connections visible from another LinkedIn profile, list ready connected accounts first, then use the returned Cluster account ID.
ethos signals linkedin-accounts
ethos signals pull linkedin_profile_connections \
--config '{"connected_account_id":"<cluster-account-id>","target_url":"https://www.linkedin.com/in/target/","max_results":500}' \
--waitThis signal is one-time only and cannot be used as a workflow trigger.
Workflow Commands
List the Slack channels visible to Cluster, then use a channel ID when creating a workflow:
ethos workflows slack-channels
ethos workflows create "New lead alerts" \
--signal post_engagers \
--config '{"post_url":"https://www.linkedin.com/posts/activity-1"}' \
--step send_to_slack:C123456Pass explicit JSON to customize the message. Supported variables are {{workflow.id}},
{{workflow.name}}, {{workflow.match_count}}, {{workflow.run_id}}, and {{workflow.run_url}}.
ethos workflows create "New lead alerts" \
--signal post_engagers \
--step 'send_to_slack:{"channel_id":"C123456","channel_name":"alerts","message_template":"{{workflow.name}} found {{workflow.match_count}} matches"}'ethos workflows update <workflow-id> --step ... replaces the complete step set with a new linear
chain. Include every existing step you want to preserve. Re-activate an active workflow after changing
its steps so new matches use the updated definition.
Table Commands
ethos tables list --limit 50 --offset 0
ethos tables get <table-id>
ethos tables import-csv ./companies.csv --entity-type company
ethos tables import-csv ./people.csv --entity-type people
ethos tables source-people <table-id> \
--filter <column-id>:contains:example \
--input-column <column-id>
ethos tables columns create-agent <table-id> \
--name "ICP fit" \
--agent-id <agent-id> \
--prompt "Score this account" \
--output-field score \
--output-field reason
ethos tables columns configure <table-id> <column-id> --name "New name"
ethos tables columns delete <table-id> <column-id>
ethos tables columns extract-json <table-id> <column-id>
ethos tables rows delete <table-id> <row-id>
ethos tables rows delete <table-id> 3 --by-index
ethos tables rows delete <table-id> 1 2 3 --by-index
ethos tables agent-edits apply <table-id> \
--instruction "Delete rows that are no longer relevant" \
--row-id <focused-row-id>
ethos tables cells set <table-id> <row-id> <column-id> --value "Manual note"
ethos tables cells set <table-id> <row-id> <column-id> --value '{"score":9}' --value-json
ethos tables runs start <table-id> <column-id> --scope first_5
ethos tables runs start <table-id> <column-id> --lower-range 1 --upper-range 50
ethos tables runs get <run-id>
ethos tables runs interrupt <run-id>
ethos tables source-people <company-table-id> \
--view-id <view-id> \
--agent-id <people-sourcing-agent-id> \
--input-column <company-domain-column-id> \
--targeting-brief "Find funding and finance contacts" \
--created-by-codex
ethos tables create-people-table <company-table-id> \
--column-id <people-finder-column-id> \
--source-column <signal-context-column-id>List And Campaign Commands
ethos lists import-from-table <people-table-id> \
--linkedin-column "LinkedIn URL" \
--custom-variable outreach_message \
--custom-variable normalized_company_name
ethos campaigns attach-list <campaign-id> <list-id>
ethos campaigns update-sequence <campaign-id> \
--first-step invitation \
--messages-json '[{"channel":"EMAIL","subject_template":"Hello {{company}}","body_template":"Email body","wait_amount":1,"wait_unit":"hours"},{"channel":"LINKEDIN","body_template":"Following up","wait_amount":2,"wait_unit":"days"}]'
ethos campaigns update <campaign-id> \
--exclude-existing-linkedin-connections true \
--withdraw-invites-after-days 14campaigns update-sequence changes future, unsent outreach. It replaces the full active sequence, so preserve any timing and step settings you do not intend to change. Existing-step edits are allowed while a campaign is running; pause it before adding or removing steps. Step types and channels cannot change after launch. Sent activity stays intact, pending removed steps are skipped, and completed contacts are not backfilled.
Sequence entries accept action_type: message (default for LINKEDIN), email (default for EMAIL), profile_visit, or like_post. profile_visit views the lead's LinkedIn profile so they see the visit; like_post likes their most recent post inside like_post_recency_days (1-365, default 30), counting reposts only when like_post_include_reposts is true, and is skipped with a visible reason when there is none. Touchpoints take no body_template, subject_template, or media_attachment and need no connection:
ethos campaigns create-with-sequence --name "Warm up first" --list-id list_1 \
--messages-json '[{"action_type":"profile_visit"},{"action_type":"like_post","wait_amount":1,"wait_unit":"days"},{"body_template":"Loved your recent post, {{first_name}}","wait_amount":2,"wait_unit":"days"}]'campaigns update changes campaign settings; pass only the flags you want to change: --name, --connected-account-id (LinkedIn sender), --email-connected-account-ids (comma-separated Gmail sender pool, or none), --audience-id (or none to detach), --delivery-mode automatic|manual (drafts only), the daily caps --linkedin-invite-daily-limit, --linkedin-message-daily-limit, --email-daily-limit (1-500 or off), and the safety settings. --withdraw-invites-after-days auto-withdraws LinkedIn invitations still pending after 1-90 days (default 14 for new campaigns); pass off to disable. campaigns sender-accounts lists the accounts to choose from.
Branching sequences, previews, readiness, and progress
The web builder saves sequences as a graph; the CLI accepts the same shape through --sequence-json on campaigns create-with-sequence and campaigns update-sequence. Steps carry key, action_type (connection_request, message, email, profile_visit, like_post, follow_company), content, wait_amount/wait_unit (the wait after the step before its default successor), acceptance_wait_days on a connection request, and reply_wait_days on a message or email that branches on reply. Edges carry source_key (null for the single entry edge), target_key, route_key (default, accepted, not_accepted, replied, no_reply), position, and an optional wait for outcome routes. Every step returned by campaigns get carries its step_id; keep it on steps you retain so sent history survives a save.
ethos campaigns get <campaign-id> # campaign, sequence_graph, journeys
ethos campaigns preview-sequence --sequence-json '{"steps":[...],"edges":[...]}'
ethos campaigns preview-sequence <campaign-id> --sequence-json '...' # also reports applies_live, requires_pause, added/kept/retired steps
ethos campaigns create-with-sequence --name "Branching" --audience-id <audience-id> --sequence-json '...'
ethos campaigns update-sequence <campaign-id> --sequence-json '...' --preview
ethos campaigns update-sequence <campaign-id> --sequence-json '...'
ethos campaigns readiness <campaign-id> # launch blockers and warnings, same checks as launch
ethos campaigns launch <campaign-id>
ethos campaigns progress <campaign-id> --wait --poll-interval 60 # contact/step counts, next send, failures
ethos campaigns pause <campaign-id>
ethos campaigns resume <campaign-id>
ethos campaigns archive <campaign-id> # the app's Delete action; cannot be undoneupdate-sequence is the builder's "Save workflow": on a draft it saves the draft, on a launched campaign it applies to future outreach immediately, so preview first. Previews return valid, structured errors, the normalised sequence, and journeys, one per outcome (accepted, not accepted, replied, no reply) with conditions and earliest day offsets; journey_summaries gives one line per path. Structure changes to a running campaign need campaigns pause first. like_post steps take like_post_recency_days and like_post_include_reposts in both shapes; there is no selector for a specific post.
lists import-from-table creates contacts from table rows with valid LinkedIn person profile URLs or identifiers. It skips missing, invalid, and duplicate LinkedIn identities, reuses matching workspace contacts by default, and preserves source row IDs plus selected custom variables.
Edit an existing list without recreating it:
ethos lists contacts <list-id> --search berlin
ethos lists add <list-id> linkedin.com/in/ada --contact-id <contact-id> --file profiles.txt
ethos lists move <list-id> <list-contact-id> <list-contact-id> --to <target-list-id>
ethos lists move <list-id> <list-contact-id> --new-list "German speakers"
ethos lists remove <list-id> <list-contact-id> <list-contact-id>lists contacts returns each person's list_contact_id, which move and remove take. add skips people already in the list. move keeps each contact's custom variables; a contact the target list already holds is only removed from the source list. remove leaves the people in the workspace and in any other lists.
When stdout is not a TTY, output is JSON by default so coding agents can parse it reliably.
Table-detail commands such as tables get, tables import-csv, and tables create-people-table include top-level table_id, column_ids, and row_ids in addition to the full nested objects. tables source-people includes top-level table_id, column_id, and run_id.
Analytics
The app, MCP, and CLI share three read operations:
ethos analytics schema
ethos analytics query --spec analytics-query.json
ethos analytics records --query-json '{"start_date":"2026-08-01","end_date":"2026-08-31","metric":"reply_rate","population":"denominator"}'Example analytics-query.json:
{
"start_date": "2026-08-01",
"end_date": "2026-08-31",
"timezone": "America/Los_Angeles",
"group_by": ["campaign", "sender", "channel"],
"scope": [{"dimension": "channel", "value": "EMAIL"}],
"comparison": "previous_period",
"interval": "day",
"sort_by": "leads_reached",
"sort_direction": "desc"
}Scope contains exact dimension/value pairs from a selected campaign or breakdown row; all conditions must match. Activity uses event dates. Rates use people reached in those dates and subsequent matching outcomes up to now, without a response deadline. Use returned totals and rate components rather than summing overlapping groups. Chart selection and visible columns stay in the app.
For local testing, use the explicit checkout CLI path in .ethos-dev/context.json
after starting make dev.
