Public Summation CLI — a first-party client of sum-api. Talks to stable /v1 routes on the public gateway, plus optional direct provider APIs for external storage (provider-specific).
| Context | Name |
|---|---|
| PyPI / package | summation-cli |
| Python import | sum_cli |
| Binary | sumcli |
| Entry point | sum_cli.cli.main:main |
Use sumcli for scripted automation and agent workflows. The summation skill (~/.agents/skills/summation) helper scripts may coexist until fully deprecated; prefer sumcli for new work.
Output uses JSON envelopes on stdout with contextual next_actions, and NDJSON for streaming commands. Output is agent-first JSON whenever stdout is not a TTY (piped, captured, or run by an agent), so the contract holds in scripted use — pipe through jq. At an interactive terminal it renders a human plain-text view instead. Force it with SUMCLI_OUTPUT=json|human (any position) or the root option --output json|human, which must precede the subcommand (sumcli --output human projects list). The human view is lossy (wide tables drop columns, noted inline) and not meant for parsing.
Requires uv and Python 3.11+ (uv can install Python for you).
uv tool install summation-cli
# or pin a release:
uv tool install summation-cli==X.Y.ZThen:
sumcli --help
sumcli update # upgrade a uv-managed install to the latest PyPI releaseCommands print a stderr notice when a newer PyPI version exists. Lookups are
cached (a day on success, 15 minutes after a failed fetch). Stdout is unchanged,
so JSON/jq still parse. Disable with SUMCLI_NO_UPDATE_CHECK=1.
sumcli update upgrades a uv-managed install only; other origins (pip, pipx,
brew) get a targeted error instead of a second copy on PATH.
Bootstrap (installs uv if needed, then uv tool install summation-cli):
curl -fsSL https://install.summation.com/sumcli | shirm https://install.summation.com/sumcli.ps1 | iexFrom cmd.exe (Windows Shell), launch the same PowerShell installer:
powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://install.summation.com/sumcli.ps1 | iex"The Summation plugin requires sumcli ≥ 0.1.4. Newer CLI releases are always compatible — sumcli update (PyPI latest) is the upgrade path. A plugin release that needs a higher floor will bump its own minVersion; this CLI does not pin an upper bound.
sumcli --version prints a JSON envelope with result.version when stdout is not a TTY (or when SUMCLI_OUTPUT=json). That is the version string plugins should parse.
| Resource | Description |
|---|---|
auth |
Inspect authentication state (whoami, status, token, login) |
config |
Profiles, active session, and ~/.summation/summation-config (use, set-project, import-env, …) |
tenant |
Organization and tenant metadata |
projects |
Project CRUD and current |
chats |
Addison conversations; SSE → NDJSON with --follow on create/reply |
reports |
Generate and verify reports (.sdoc); file ops via files |
playbooks |
Playbook discovery |
schedules |
Recurring playbook runs (CRUD, pause/resume, run, runs); create may require workflows |
workflows |
Multi-step automations (typed graphs: create/update/activate/run, versions, node-types) |
files |
Project-scoped files (upload, download, list, delete) |
filesystem |
Connected filesystem roots such as SharePoint |
catalog |
Project catalog entries (tables/views attached to a project) |
connections |
Data source connections (CRUD, test, browse, datasets, attach-datasets, detach-dataset, snapshot, snapshots) and app connectors (app-*) |
tables |
Grid tables and CSV import (tables import); row loads via append or upsert; also data, import-status, catalog helpers |
views |
Summation views |
grid |
Grid status, sync, lineage, and table creation (create --kind calc or data) |
queries |
Read-only SQL execution (queries run) |
verification-tests |
Validate, upload, attach, preview, and detach custom verification tests |
Run sumcli | jq '.result.resources' for the live command tree with action blurbs, or sumcli <resource> --help for flags.
Validate bundles entirely offline, then use the active profile and normal bearer authentication for the managed lifecycle:
sumcli verification-tests validate --bundle ./tests.yaml
sumcli verification-tests upload --bundle ./tests.yaml
sumcli verification-tests list --subject-type deck
sumcli verification-tests attach --scope project --subject-type deck \
--op add --custom-test-id cvt-...
sumcli verification-tests list-attachments --scope project --subject-type deck
sumcli verification-tests preview --scope project --subject-type deck
sumcli verification-tests detach vta-... --scope project --confirmProject scope uses the profile's default project when --project is omitted. Cross-org calls use --target-org ORG and require an explicit project for project scope; identity always comes from the bearer token. Add --dry-run to upload, attach, or detach to validate and print the exact request without authentication or network access. A removal overlay (attach --op remove --target-ref ... --confirm) suppresses a test in resolution and requires --confirm because it turns off a test that currently runs; detach ... --confirm soft-removes the attachment record itself. Dry runs never require --confirm.
Editable install from this repo:
uv tool install .
# or: uv pip install -e .Releases are tag-driven. main is the next unreleased line of development.
Merging without a version bump does not publish anything. To ship:
- Bump
__version__insum_cli/__init__.py(and merge that tomain). - Tag the release commit and push the tag:
git tag vX.Y.Z git push origin vX.Y.Z
.github/workflows/release.ymlre-runs tests, checks the tag matches__version__, builds, publishes to PyPI via Trusted Publishing (OIDC), creates a GitHub Release with the wheel/sdist, then dispatches the monoreposumcli-pin-bump.yamlworkflow for the exact published version.
The tagged commit must be on main. A v* tag can point at any commit, so the
workflow also checks that the commit is an ancestor of origin/main and fails
if it is not. Tag after the version bump merges, not before.
One-time setup: add a PyPI Trusted Publisher for this repo
(workflow: release.yml, environment: pypi) and create a GitHub Environment
named pypi. Add required reviewers to that environment; the approval is the
last human gate before a publish.
One-time setup for the SUM-6166 companion dispatch: install the GitHub App backed
by TS_PROTO_GEN_APP_ID / TS_PROTO_GEN_APP_KEY with access to
summationai/code, grant it Actions: write on that repository, and define
TS_PROTO_GEN_APP_ID as an Actions variable plus TS_PROTO_GEN_APP_KEY as an
Actions secret available to this repo. The monorepo sumcli-pin-bump.yaml
workflow must already be merged to summationai/code main before this CLI
hook is merged, or the dispatch target will not exist. The dispatch job must
fail rather than silently skip if those prerequisites are missing. Manual
recovery after a successful publish:
gh workflow run sumcli-pin-bump.yaml -R summationai/code -f version=X.Y.ZLocal/emergency publishes (and TestPyPI dry runs) still work with
./scripts/publish.sh:
export UV_PUBLISH_PASSWORD_TEST='pypi-...' # TestPyPI
export UV_PUBLISH_PASSWORD='pypi-...' # production (prefer CI)
./scripts/publish.sh # TestPyPI
./scripts/publish.sh --production # real PyPI (type the version to confirm)A published version is permanent and can never be replaced.
sumcli config set-profile onboard2 \
--base-url https://api-<tenant>.summation.com
sumcli config use onboard2
sumcli --profile onboard2 auth login
sumcli --profile onboard2 auth whoami | jq .sumcli config set-profile onboard2 \
--base-url https://api-<tenant>.summation.com \
--client-id "$SUM_API_CLIENT_ID" \
--client-secret "$SUM_API_CLIENT_SECRET"
sumcli config use onboard2
sumcli --profile onboard2 auth login --m2m
sumcli auth whoami | jq .
sumcli projects list | jq '.result.projects'Two ways: a one-shot direct ingest, or an explicit two-step that goes through the project file tree first.
One-shot (recommended when you don't need the file in the project tree):
sumcli tables import --local --path ./Customers.csv --table customersUploads bytes → previews schema → materializes a new grid table. Outputs NDJSON ending with importStatus: SUCCESS and a tbl-... ID. Server auto-detects column types.
Two-step (when you want the CSV in the project file tree too):
# 1. Upload the CSV into the project at /Customers.csv.
sumcli files upload ./Customers.csv
# 2. Promote it from the project tree into the grid.
sumcli tables import --remote --path /Customers.csv --table customersStep 2 also accepts --file-id file-... if you have the ID directly. Internally, --remote mode downloads the file's bytes to a temp file, then runs the same upload+import flow as --local (sum-api has no direct project-file → grid endpoint today).
tables import and grid create --kind calc both derive a table from data that already
exists. When your code owns the rows instead — app state, an operator log, a
suppression list — create an empty data table from a column schema, then load rows
with tables upsert:
sumcli grid create ops_log --kind data \
--column event_id:uuid:notnull \
--column op:string \
--column count:integer \
--column noted_at:datetime \
--key-column event_idEach --column is name:type[:null|notnull], and order is kept. Types: string,
integer, decimal, big_decimal, boolean, date, datetime, json, uuid.
Columns are nullable unless you pass :notnull. For a longer schema, use
--columns-file cols.json with a JSON array instead:
[{"name": "event_id", "type": "uuid", "nullable": false},
{"name": "op", "type": "string"}]The table accepts rows as soon as the create returns:
sumcli tables upsert tbl-... --rows '[{"event_id": "...", "op": "suppress", "count": 1}]'tables append vs tables upsert: both hit /v1/tables/{id}/rows, different methods.
| Command | API | Rows must include |
|---|---|---|
tables upsert |
PUT |
Business-key columns only (event_id, …) — use for kind=data tables |
tables append |
POST |
Primary key s_id (caller-assigned, append-only) |
--key-column on create names the business key matched on upsert, not the physical
primary key. Every data table already has an integer s_id primary key and an
s_created_at timestamp, added for you — declaring either in --column is refused.
A single create takes at most 50 columns.
If the file is already in the project (uploaded by someone else, dropped via the UI, etc.):
sumcli files list | jq '.result.files[] | select(.fileName | endswith(".csv"))'
sumcli tables import --remote --path "/Order_Details.csv" --table order_detailsAfter tables import succeeds, the new table lives in the tenant grid but is not attached to the project catalog. Attach it to make it visible in catalog list and queryable as a project resource:
sumcli tables show tbl-... # schema + columns
sumcli tables data tbl-... | jq '.result.data.rows[:5]' # sample rows
sumcli catalog attach --source-type table --source-id tbl-... # link to current project
sumcli catalog list # confirm linkage
sumcli catalog detach --confirm file-... # remove the catalog entry
sumcli tables delete --confirm tbl-... # remove from gridNote:
tables deleteremoves the underlying grid table but does not auto-cascade the project catalog entry that referenced it. Detach the entry separately withcatalog detach <file_id> --confirm.
Schedules target playbooks only — kind is playbook in the API. Playbook ids come back as fileId from playbooks list. On tenants with workflows enabled, schedules create may return 403 use_workflows — use workflows instead (existing schedules remain usable).
sumcli schedules create --project prj-... --playbook file-... \
--type daily --time-of-day 09:30 --zone America/Los_Angeles \
--email you@example.com
sumcli schedules list --project prj-...
sumcli schedules pause schedule_... # stop without deleting
sumcli schedules run schedule_... --confirm # trigger one off-cadence run (sends email)
sumcli schedules runs schedule_... # run history
sumcli schedules delete schedule_... --confirm--type accepts cron, interval, one_time, daily, weekly, biweekly, monthly, month_end, and yearly. Supply the fields each type needs: --cron, --every-minutes, --run-date, --day, --day-of-month, --month.
Note:
PUT /v1/schedules/{id}replaces the whole schedule, soschedules updatere-sends every cadence flag. Config is preserved: the command reads the schedule first and carries over--param,--output-folder,--max-concurrent-runs, and--pausedwhen you omit them. This merge is deliberate —email_recipients,params, andoutput_confighave no server-side default, so a cadence-only update would otherwise stop all email delivery.
Typed-graph automations under /v1/workflows (feature-gated). Author graph.json / triggers.json from workflows node-types, then create → activate → run.
sumcli workflows node-types
sumcli workflows create --project prj-... --title "Weekly" \
--graph-file graph.json --triggers-file triggers.json
sumcli workflows activate wf_... --expected-revision N --confirm
sumcli workflows run wf_... --confirm # --version from activeVersionId when omitted
sumcli workflows runs wf_...chats create, chats reply, reports generate, reports verify, grid push, and tables import all support --wait/--no-wait (and --follow where applicable). See Long-running commands below.
Most users should use device login; admin-managed accounts can use M2M. Power users can define several environment accounts in the config file. Each profile is a tenant + API host + credentials/session state (not a Ramp-style --env toggle on one identity). Name profiles {tenant}_{env} when you have multiple deployments (e.g. tenant sandbox, staging, production).
Config file: ~/.summation/summation-config (TOML), overridable with SUMMATION_CONFIG_FILE.
[_meta]
active_profile = "tenant_staging"
[tenant_sandbox]
base_url = "https://sandbox-api-tenant.summation.com"
client_id = "..."
client_secret = "..."
[tenant_staging]
base_url = "https://staging-api-tenant.summation.com"
client_id = "..."
client_secret = "..."
default_project = "prj-..." # optional; see project resolution below
[tenant_production]
base_url = "https://api-tenant.summation.com"
client_id = "..."
client_secret = "..."Optional per-profile fields: device_login_credential, access_token, token_expires_at, m2m_scope, default_project.
| Command | Description |
|---|---|
config path |
Print config file path |
config list |
List profiles (secrets not shown) |
config show [profile] |
Show one profile from file (secrets redacted) |
config active |
Resolved effective config: active profile, account, default project, credentials |
config import-env |
Import SUM_API_* from an env file into ~/.summation/summation-config |
config set-profile |
Create or replace a profile (--confirm not required) |
config copy-profile |
Clone a profile |
config delete-profile |
Remove a profile (--confirm) |
set-profile options: --base-url, optional --client-id + --client-secret, --default-project, --m2m-scope, --login/--no-login.
- With only
--base-url,set-profilecreates a device-login-ready profile. Then runsumcli --profile <name> auth login. - With both
--client-idand--client-secret, the profile can also usesumcli --profile <name> auth login --m2m. --loginonly performs the M2M exchange path when M2M credentials are present.
Profile switching and the active default project live under config (there is no separate context resource):
| Command | Description |
|---|---|
config use <profile> |
Set _meta.active_profile; optional --project writes default_project |
config set-project <id> |
Set default_project for the active (or --profile) profile |
config active |
Active profile, account, base URL, default project, resolved credentials |
config clear-project |
Clear default_project for the active (or --profile) profile |
Switch environment account:
sumcli config use tenant_staging— updates shared config (interactive)sumcli config use tenant_staging --project prj-...— profile + default project in one stepsumcli --profile tenant_staging projects list— one-off;--profile/--base-urlmust come before the subcommandexport SUMMATION_PROFILE=tenant_staging— per-process (safe for parallel agents)
Parallel agents: do not call config use on a shared ~/.summation/summation-config. Pass --profile, and/or set SUMMATION_PROFILE / SUMMATION_PROJECT in each subprocess. Prefer profiles without a file default_project when using SUMMATION_PROJECT, or pass --project on each command (see precedence below).
Precedence is field-specific (there is no single global env-beats-file rule).
- CLI
--profile SUMMATION_PROFILE[_meta].active_profilein the config file"default"
- CLI
--base-url SUM_API_BASE_URL- Profile section
base_urlin the config file https://api.summation.com
- Matching
SUM_API_*environment variable - Profile section in the config file
Auth behavior (auth.py):
- Profile
device_login_credential— used as-is as the bearer token SUM_API_ACCESS_TOKEN/ profileaccess_token— used as-is (no M2M exchange)SUM_API_CLIENT_ID+SUM_API_CLIENT_SECRET/ profileclient_id+client_secret— exchanged atPOST /v1/auth/m2m/token(optionalscopefromSUM_API_M2M_SCOPE/ profilem2m_scope)
Identity comes from the bearer token only — never x-org-id / x-user-id.
- CLI
--projecton the command (highest) - Profile
default_projectin the config file SUMMATION_PROJECTenvironment variable
Explicit --project always wins. When both file and env set a default, the file value wins over SUMMATION_PROJECT.
| Variable | Purpose |
|---|---|
SUMMATION_CONFIG_FILE |
Path to TOML config (default ~/.summation/summation-config) |
SUMMATION_PROFILE |
Active profile name |
SUMMATION_PROJECT |
Default project ID when the profile has no default_project |
SUM_API_BASE_URL |
API host (no trailing slash required; stripped) |
SUM_API_CLIENT_ID |
M2M client ID |
SUM_API_CLIENT_SECRET |
M2M client secret |
SUM_API_ACCESS_TOKEN |
Static bearer token (skips M2M exchange) |
SUM_API_M2M_SCOPE |
Optional scope on M2M token request |
SUMCLI_INTENT |
Default --intent (human's request, their words when possible) |
SUMCLI_NO_INTENT |
Do not send X-Summation-Intent, even if --intent / SUMCLI_INTENT is set |
SHAREPOINT_TENANT_ID |
Azure AD tenant for SharePoint app-only auth |
SHAREPOINT_CLIENT_ID |
SharePoint app client id (falls back to CLIENT_ID) |
SHAREPOINT_CLIENT_SECRET |
SharePoint app secret (falls back to CLIENT_SECRET) |
SHAREPOINT_SITE_URL |
SharePoint site, e.g. host.sharepoint.com:/sites/Name |
SHAREPOINT_ROOT |
Default drive id (quote in shell/.env if it contains !) |
SHAREPOINT_PATH |
Default folder item id |
Unlike the rest of sumcli, external storage commands talk directly to external storage providers (SharePoint via Microsoft Graph today). They do not use sum-api or the active M2M profile.
Provider-specific external storage commands exist for SharePoint, but are omitted from this public README to keep it platform-agnostic.
Provider credentials and default root/path are stored in ~/.summation/summation-config (provider-specific config).
Bare invocation prints the full command tree as JSON. Resource names and actions come from the live Typer app; action blurbs for API-backed commands are derived from the bundled OpenAPI snapshot (sum_cli/data/openapi_snapshot.json).
sumcli | jq '.result.resources | keys'
sumcli | jq '.result.resources.projects'
sumcli | jq '.result.resources.projects.actions'
sumcli projects --help # per-command flags and Typer help stringssumcli [--intent TEXT] [--profile NAME] [--base-url URL] <resource> <action> [--options]
--intent is the human's request in their own words when possible — not a summary of the command. It is sent to sum-api as X-Summation-Intent. It is optional: omitting it in machine mode (piped, or --output json) prints a warning on stderr and the command still runs, so unattended callers such as Dagster ops keep working. Agents should always pass it — without it a run cannot be joined to a goal. SUMCLI_INTENT sets the string for a session. --intent is a root option and must precede the subcommand. SUMCLI_NO_INTENT=1 is an org-level kill switch: the header is not attached, the missing-intent warning is skipped, and an oversized value is not refused.
No warning at all for: discovery (sumcli with no args), --help, --version, update, and the auth, config, and filesystem groups. auth and config set up the session before there is a goal to state; filesystem talks to the external storage provider with that provider's credentials and never reaches sum-api. The value is normalized to one line, control characters are removed, and it is limited to 500 bytes after encoding — so non-ASCII text gets fewer than 500 characters. An oversized intent is refused with INTENT_TOO_LONG, since that value would go on the wire.
Project-scoped commands accept --project when no default project is configured.
python -m pytest -q
# equivalent:
PYTHONPATH=. pytest -qOpenAPI drift is guarded offline against the bundled snapshot at sum_cli/data/openapi_snapshot.json:
python -m pytest tests/test_openapi_contract.py tests/test_load_spec.py -q
# refresh snapshot after sum-api ships new routes:
python scripts/refresh_openapi.py
# verify bundled snapshot matches production (nightly automation + manual pre-release):
python scripts/refresh_openapi.py --checkPer-PR CI gates on the offline contract tests above only. Production reconciliation runs on a schedule via .github/workflows/sumcli-openapi-snapshot.yaml so unrelated backend PRs are not reddened when sum-api deploys ahead of the snapshot.
Command-tree action blurbs for API-backed commands are derived from the snapshot at runtime via sum_cli/openapi_doc.py; config and other local-only actions stay hand-written there. Composite commands (tables import, reports verify) have known doc/route alignment gaps — see comments in openapi_doc.py.
- OpenAPI at
${SUM_API_BASE_URL}/openapi.jsonis the contract source of truth;sum_cli/data/openapi_snapshot.jsonis the offline copy shipped in the wheel and reconciled bytests/test_openapi_contract.py(CLI call sites must exist in the spec; uncovered spec operations must be allow-listed insum_cli/openapi_doc.py). - No imports from sum-api service code or gRPC clients.
- Destructive commands require
--confirm:projects delete,files delete,views delete,tables delete,connections delete,connections detach-dataset,connections app-delete,schedules delete,schedules run,workflows activate,workflows run,catalog detach,verification-tests attach(removal overlays only),verification-tests detach,filesystem delete,config delete-profile.filesystem uploadrequires--confirmonly when it overwrites an existing file.schedules run/workflows run/workflows activateare gated because they can deliver real email/Slack immediately. sumcli auth statuscallsGET /v1/auth/statusonly (not an alias forwhoami).sumcli auth tokenexchanges credentials if needed and prints a redacted token plus length.- List commands default to 50 items unless
--countis set (showing,total,truncatedin the result).
Commands talk only to the Summation API /v1 routes, with these exceptions:
tables importPUTs file bytes directly to a pre-signed URL that the API returns. The CLI never constructs that URL itself.filesystem(SharePoint) sends credentials tologin.microsoftonline.comand file bytes tograph.microsoft.com; it does not go through sum-api.
| Flag | Meaning |
|---|---|
--wait (default) |
Drain the server's SSE stream to completion, then return the final envelope |
--no-wait |
Drain the same stream, but never print progress lines |
--follow |
Stream NDJSON progress to stdout while the operation runs (requires --wait) |
Uses --wait / --follow: chats create, chats reply, reports generate, reports verify, grid push.
Defaults: --wait on everywhere above. reports generate and reports verify default --follow on (SSE from sum-api), so they stream NDJSON; pass --no-follow to wait for completion and print one final envelope. chats create, chats reply, and grid push default --follow off and print one JSON envelope unless you pass --follow.
--follow requires --wait. Passing --no-wait --follow together is rejected with an INVALID_FLAGS error and exit 1 on every command above. Passing --no-wait on its own is always valid, including on the commands that follow by default.
Caveat: today
--no-waitand--waitproduce the same network behavior — both consume the server's SSE stream to completion, since sum-api has no fire-and-forget mode. The only difference is whether intermediate--followprogress is printed. If a true async response is added server-side,--no-waitwill switch to it without a CLI change.
Examples:
sumcli reports generate -m "Q4 summary" # wait, NDJSON stream (default follow)
sumcli reports generate -m "Q4 summary" --no-follow # wait, single JSON response
sumcli reports generate -m "Q4 summary" --no-wait # silent drain, final envelope only
sumcli chats create -m "hello" # wait, single JSON response (follow off by default)
sumcli chats create -m "hello" --follow # wait, NDJSON streamtables import uses --wait/--no-wait only (no --follow). With --wait, stdout is NDJSON (step, progress, then a terminal result or error line). Failed uploads or import statuses FAILED / ERROR emit an error terminal and exit code 1. On success the result carries import_id and the resolved table_id (looked up from /v1/tables by name, since /v1/table-imports returns only the import status), so you can pipe straight into tables show <table_id>.
chats events always streams NDJSON (--raw-sse optional); stream errors exit 1.
- Success and validation errors print one JSON envelope; failures use
exit 1(emit_errororSystemExitafter a stream error). - With
--follow, intermediate lines are NDJSON (type:start,text,step,progress,log, …). The last line is a terminal envelope:typeresultorerror, spreading the sameok/error/fixfields as non-streaming output. - SSE
errorevents and transport failures produce a terminalerrorenvelope and exit 1 (without printing a second JSON blob).
AuthErrorandApiErrorare caught inmain()and emitted as structured envelopes.- Resource commands call
emit_error()for validation (NO_PROJECT,CONFIRM_REQUIRED,INVALID_FLAGS,IMPORT_FAILED, etc.). - Unexpected exceptions become
INTERNAL_ERRORenvelopes.
M2M tokens from /v1/auth/m2m/token use the Stytch OAuth shape (expires_in seconds, typically 3600). The CLI caches in-process until expires_in - 60s skew (TOKEN_CACHE_SKEW_SECONDS), then refreshes.
Cache key includes profile, base URL, client_id, client_secret, and m2m_scope for M2M (so rotated secrets or scopes fetch a new token). Static access_token credentials are cached separately by token value.
If the response omits expires_in, TTL falls back to 300 seconds (DEFAULT_M2M_TTL_SECONDS) after JWT exp parsing when possible.