C1 Interface — and it looks like cli. Get it?
Beta — stabilizing toward 1.0. The core commands, flags, and output formats are stable; smaller changes may still land before 1.0.
A command-line interface for the C1 API designed for AI agents. Structured output (NDJSON/JSON), built-in API docs, and auto-pagination. For a human-friendly CLI, see cone.
# Install
go install github.com/ConductorOne/c1i@latest
# Log in (opens browser)
c1i auth login --url mycompany.conductor.one
# List users
c1i users list
# Explore the API — no credentials needed
c1i docs search "access reviews"
c1i docs endpoints --filter taskc1i users list [--query <text>] [--email <email>] [--status enabled|disabled|deleted] [--page-size N] [--page-token TOKEN] [--limit N]
c1i users get <user-id>c1i apps list [--page-size N] [--page-token TOKEN] [--limit N]
c1i apps get <app-id>c1i accounts list --app-id <id> [--status enabled|disabled|deleted] [--type user|service_account|system_account] [--unmapped-only] [--query <text>] [--page-size N] [--page-token TOKEN] [--limit N]
c1i accounts set-owner <app-user-id> --app-id <id> --user-id <id>c1i entitlements list [--app-id <id>] [--query <text>] [--page-size N] [--page-token TOKEN] [--limit N]
c1i entitlements get <entitlement-id> --app-id <id>c1i grants list --app-id <id> --entitlement-id <id> # who holds an entitlement
c1i grants list --user-id <id> # what a C1 identity has, across apps
c1i grants list --app-user-id <id> # what an app account holds
c1i grants list --app-id <id> # every grant in an appGrants are the bindings between accounts/users and entitlements. At least one
filter is required. Each NDJSON row includes the entitlement, the account
(app_user_*) and its identity_user_id, timestamps (created_at,
deprovision_at), and grant_source_count — 0 for a direct grant, or the
number of groups/roles the access is inherited through.
c1i tasks list [--state open|closed] [--query <text>] [--assigned-to-me] [--page-size N] [--page-token TOKEN] [--limit N]
c1i tasks approve <task-id> [--policy-step-id <id>] [--comment <text>]
c1i tasks deny <task-id> [--policy-step-id <id>] [--comment <text>]
c1i tasks comment <task-id> --comment <text>approve/deny target a specific policy step. If --policy-step-id is
omitted, the task's currently executing step is fetched and used automatically
for both — but approve requires a resolvable step and errors if it can't
find one, while deny proceeds without a step if none can be derived.
c1i connectors list --app-id <id> [--page-size N] [--page-token TOKEN] [--limit N]c1i functions list [--published-only | --draft-only] [--page-size N] [--page-token TOKEN] [--limit N]
c1i functions get <function-id>
c1i functions source <function-id> [--commit <id>] [--out-dir <path>]
c1i functions commits <function-id> [--page-size N] [--page-token TOKEN] [--limit N]
c1i functions usage <function-id>functions source auto-resolves the function's published commit (falling back to its head/latest draft) and base64-decodes the source files. Without --out-dir, each file is printed to stdout with a // ===== <name> ===== delimiter; with --out-dir, files are written to disk. functions usage scans every automation and emits one row per step that calls the given function — useful before deleting a draft.
c1i automations list [--enabled-only] [--calls-function <fid>] [--page-size N] [--page-token TOKEN] [--limit N]
c1i automations get <automation-id>
c1i automations executions list [--state done|error|pending|...] [--template-id <tid>] [--page-size N] [--page-token TOKEN] [--limit N]Each automations list row includes function_ids (every distinct function the automation invokes), so --calls-function can answer "which automations call function X?". executions list --state accepts the short forms (done, error, pending, ...) or the full AUTOMATION_EXECUTION_STATE_* enum; state and template filtering are applied client-side, so pair a narrow filter with --limit to bound the work.
Drive the MCP admin surface (servers, tools, toolsets, and bindings). Most commands take --app-id; mcp servers commands take the server's <connector-id> positionally, while tool/toolset/binding commands scope to a server with --connector-id.
# Servers (register, configure, and inspect MCP servers)
c1i mcp servers list --app-id <id> [--page-size N] [--limit N]
c1i mcp servers get <connector-id> --app-id <id>
c1i mcp servers search --app-id <id> [--query <text>] [--tool-state approved|pending|disabled|removed] [--include-last-called-at] [--limit N]
c1i mcp servers register --app-id <id> --type hosted --display-name <name> --catalog-id <cid> [--auth ... ] [--config-field k=v ...]
c1i mcp servers register --app-id <id> --type external --display-name <name> --url <url> [--transport streamable-http|sse] [--auth ...]
c1i mcp servers update <connector-id> --app-id <id> [--display-name <name>] [--description <text>] [--data-sensitivity ...] [--tool-prefix <p>] [--require-tool-approval]
c1i mcp servers update-credentials <connector-id> --app-id <id> --type hosted|external [--auth ...] [--update-mask <paths>]
c1i mcp servers delete <connector-id> --app-id <id>
c1i mcp servers resync-tools <connector-id> --app-id <id> # EXTERNAL only; 400 on HOSTED
c1i mcp servers test-connection (--url <url> [--transport ...] [--auth ...] | <connector-id> --app-id <id>) # EXTERNAL only; 400 on HOSTED
c1i mcp servers discover-oidc --issuer-url <url>
c1i mcp servers catalog list [--query <text>] [--page-size N] [--limit N]
c1i mcp servers catalog get <catalog-id>
c1i mcp servers connections list [--page-size N] [--limit N]
# Tools
c1i mcp tools list --app-id <id> --connector-id <id> [--page-size N] [--page-token TOKEN] [--limit N]
c1i mcp tools get <tool-id> --app-id <id> --connector-id <id>
c1i mcp tools search --app-id <id> --connector-id <id> [--query <text>] [--state ...] [--classification ...] [--page-size N] [--limit N]
c1i mcp tools approve <tool-id> --app-id <id> --connector-id <id> [--state approved|disabled|pending]
c1i mcp tools delete <tool-id> --app-id <id> --connector-id <id>
c1i mcp tools history <tool-id> --app-id <id> --connector-id <id> [--page-size N] [--limit N]
# Toolsets (admin-curated tool groupings; one AppEntitlement per toolset)
c1i mcp toolsets list --app-id <id> --connector-id <id> [--page-size N] [--limit N]
c1i mcp toolsets get <toolset-id> --app-id <id> --connector-id <id>
c1i mcp toolsets create --app-id <id> --connector-id <id> --display-name <name> [--description <text>]
c1i mcp toolsets update <toolset-id> --app-id <id> --connector-id <id> [--display-name <name>] [--description <text>]
c1i mcp toolsets delete <toolset-id> --app-id <id> --connector-id <id>
c1i mcp toolsets get-by-entitlement <app-entitlement-id> --app-id <id>
c1i mcp toolsets requestable-connectors <user-id>
# Bindings (which tools belong to which toolset)
c1i mcp bindings list --app-id <id> --connector-id <id> --toolset-id <tid> [--page-size N] [--limit N]
c1i mcp bindings create --app-id <id> --connector-id <id> --toolset-id <tid> --tool-id <id> [--tool-id <id> ...]
c1i mcp bindings delete --app-id <id> --connector-id <id> --toolset-id <tid> --tool-id <id> [--tool-id <id> ...]
c1i mcp bindings by-tools --app-id <id> --connector-id <id> --tool-id <id> [--tool-id <id> ...]
c1i mcp bindings history --app-id <id> --connector-id <id> (--toolset-id <tid> | --tool-id <id>) [--page-size N] [--limit N]
# Gateway (verify end to end: list and invoke tools over the live MCP gateway)
c1i mcp gateway list-tools [--full] [--gateway-url <url>]
c1i mcp gateway call <tool-name> [--args '{"k":"v"}'] [--gateway-url <url>]Auth for register / update-credentials: convenience flags cover the simple methods — --auth none, --auth bearer-token --bearer-token TOKEN, --auth custom-header --header-name NAME --header-value VALUE, --auth basic-auth --basic-auth-username USER --basic-auth-password PASS. For OAuth2 / AWS SigV4 / Google service-account auth, pass the full config object via --hosted-config-file / --external-config-file (JSON file, or - for stdin) — generate a ready-to-edit skeleton with --print-config-template --auth <method> [--type hosted] instead of hand-writing it. Secrets are sealed server-side; reads only ever return *_configured booleans, never the values.
mcp tools approve is the standard post-registration step: newly discovered tools (from register or resync-tools) start in PENDING_REVIEW, and an admin approves each one for the gateway to proxy calls. History endpoints return records newest-first.
mcp gateway closes the configure-then-verify loop: after registering a server and approving its tools, list-tools runs the MCP handshake against the live gateway and shows what's actually callable, and call invokes a tool and prints its result. The gateway URL defaults to the -mcp host derived from --url (e.g. acme.conductor.one → acme-mcp.conductor.one/v1); override with --gateway-url. Your standard C1 token is accepted by the gateway, so no extra auth is needed. call always prints the full result, but exits 7 (not 0) when the result itself reports isError: true — the call succeeded, the tool didn't.
c1i requests create grant --app-id <id> --entitlement-id <eid> [--user-id <uid>] [--description <text>] [--duration <duration>] [--emergency]
c1i requests create revoke --app-id <id> --entitlement-id <eid> [--user-id <uid>] [--description <text>]
c1i requests list [--user-id <id> | --all] [--app-id <id>] [--entitlement-id <id>] [--state open|closed] [--type grant|revoke] [--page-size N] [--page-token TOKEN] [--limit N]
c1i requests get <request-id>On create, --user-id defaults to the authenticated user when omitted.
requests list is the requester lens on access requests (the grant/revoke tasks
you file): by default it shows requests you opened or are the subject of —
complementing tasks list, which is the approver's My Work lens. Use --user-id
to scope to another user or --all for every request in the tenant. requests get fetches a single request (the task_id returned by requests create) as
pretty JSON, including its current policy step and outcome.
c1i export events [--since <rfc3339>] [--until <rfc3339>] [--since-event-uid <uid>] [--sort asc|desc] [--page-size N] [--page-token TOKEN] [--limit N]export events dumps the C1 system log — OCSF-formatted audit events — as an
NDJSON stream (one event per line), auto-paginating through the whole result.
Redirect it to a file to archive events or ship them to an external system:
c1i export events > audit.ndjson # everything, oldest first
c1i export events --since 2026-07-01T00:00:00Z --until 2026-07-08T00:00:00Z
c1i export events --since-event-uid <last-uid> # incremental sync--sort defaults to asc (chronological), which pairs with --since-event-uid
for incremental sync. --fields works on events too (e.g. --fields activity_name,actor.user.email_addr,time).
# GET request
c1i api --path /api/v1/apps
# POST request
c1i api --path /api/v1/search/users --body '{"pageSize":10}'
# Other methods — --method takes GET, POST, PUT, PATCH, or DELETE
c1i api --path /api/v1/apps/<app>/connectors/<conn>/mcp_tools/<id> --method DELETE
# DELETE normally refuses a body; some endpoints (e.g. remove-membership)
# require one, so opt in explicitly
c1i api --path /api/v1/apps/<app>/entitlements/<ent>/remove-membership \
--method DELETE --body '{"appUserId":"<app-user>"}' --allow-delete-body
# Read the body from a file, or stdin with "-"
c1i api --path /api/v1/search/users --body-file query.json
echo '{"pageSize":10}' | c1i api --path /api/v1/search/users --body-file -
# Add query params and headers (both repeatable)
c1i api --path /api/v1/apps --query page_size=5 --header X-Request-Id=abc123
# Auto-paginate through all results (NDJSON output, one item per line)
c1i api --path /api/v1/apps --paginate
# Force the array field to drain when auto-detection picks the wrong one
c1i api --path /api/v1/automation_executions --paginate --list-key automationExecutionsThe method defaults to GET, or POST when a body is set; pass --method for
PUT/PATCH/DELETE. The body comes from --body (inline JSON) or --body-file (a
file, or - for stdin) — the two are mutually exclusive. GET and DELETE refuse
a body by default (a body on either is more likely a mistake than intent); pass
--allow-delete-body to lift that for DELETE specifically, for the handful of
C1 endpoints that require one. --query key=value and
--header key=value are both repeatable. When --paginate is used, each page's first array-valued field is unwrapped and each item is emitted as a single line of NDJSON — the same format used by list commands. This covers both the canonical list key and typed keys like automationExecutions; use --list-key <field> to force a specific field. If the server returns the same nextPageToken twice in a row, c1i aborts with an error rather than looping forever. Without --paginate, the full JSON response is pretty-printed.
The docs commands require no C1 credentials — agents can use them to explore the API before authenticating.
# Print the agent bootstrap doc: output contracts, exit codes, when to
# prefer first-class commands over raw API calls (write to a file with --output)
c1i docs agents [--output AGENTS.md]
# Search documentation
c1i docs search "access reviews"
# Fetch a documentation page
c1i docs page product/admin/campaigns
# List API endpoints (filtered)
c1i docs endpoints --filter task
# Show full request/response schema for an endpoint
c1i docs endpoint /api/v1/search/tasks
# Dump the raw OpenAPI spec
c1i docs openapi
# Print an embedded, task-oriented runbook (list names if omitted)
c1i docs guide
c1i docs guide register-mcp-serverdocs guide is embedded static content (no network call), unlike docs search / docs page which hit the C1 documentation site. Guides ship in two families: registering and operating MCP servers (register-mcp-server, assign-toolset-everyone, test-mcp-gateway, delegate-entitlement-provisioning) and everyday app/access-request workflows (configure-new-app, request-access, inspect-and-approve-task). Run c1i docs guide with no argument for the full, current list.
docs skill is kept as an alias of docs agents for backward compatibility;
both print identical output.
- List commands (
users list,apps list, etc.) output NDJSON (one JSON object per line). apioutputs pretty-printed JSON. With--paginate, outputs NDJSON (one list item per line).docscommands output NDJSON (search,endpoints), pretty JSON (endpoint,openapiis YAML), or plain text (page).- List commands auto-paginate by default. Pass
--page-tokento fetch a single page manually. --page-sizecontrols the per-call batch size (max 100). Use--limit Nto cap the total number of results emitted; auto-pagination stops fetching new pages once the cap is reached.
--fields trims every emitted JSON object to just the keys you name — a big
token saver when an agent only needs a couple of fields from a large list.
# Only id and email from each user
c1i users list --fields id,email
# Dot-paths select nested fields; nesting is preserved in the output
c1i api --path /api/v1/apps --paginate --fields id,displayName
c1i functions get <id> --fields id,displayName,publishedCommitId- Comma-separated; use dot-paths (
user.email) for nested access. - Matches the emitted keys, trying an exact match first, then falling back
to a case- and separator-insensitive match. So
--fields displayNameresolves whether the output usesdisplayName(single-object reads) ordisplay_name(list rows); the output keeps the source key's own spelling. - Single-object
getcommands pass through the API response as-is, which wraps the resource under the endpoint's own top-level key (function,app,automation,userView.user, ...). You don't need to know that key: a name that doesn't match at the top level is also searched for inside the wrapper, so--fields id,displayNameonfunctions getfindsfunction.id/function.displayNameautomatically. The full path (--fields function.id) still works too and is tried first. If the same name exists at more than one depth, the shallowest match wins; a tie at the same depth resolves to the alphabetically first full path, deterministically. - A
--fieldsspec that matches nothing at all in the response (a typo, or a field that truly doesn't exist) is a usage error (exit2), not a silent{}. This is a zero-match check only:--fields id,dispalyName(typo) still exits0and silently returns just{"id": ...}— the misspelled field is dropped with no error and no other signal that it didn't match anything. This is deliberate, not a gap in the check:--fields/C1I_FIELDSis a persistent, session-wide setting, so one spec is routinely applied across many differently-shaped responses; erroring on any unmatched name would make a session-wideC1I_FIELDSfail on every command whose response happens to lack one of the names. Double-check the spelling of every name you pass — the tool only catches getting all of them wrong. - Missing fields are silently omitted, so requesting a superset is safe.
- Also settable via
C1I_FIELDS. Applies to read output — list commands,api, and single-objectgetcommands. Mutation confirmations (create/update/delete) are never projected, so a session-wideC1I_FIELDScan't hide their status.
On failure, c1i writes an error to stderr and exits with a code an agent can
branch on without parsing text:
| Code | Meaning |
|---|---|
0 |
success |
1 |
generic / unclassified error |
2 |
usage error (bad flags or arguments) |
3 |
not authenticated, or API returned 401/403 |
4 |
API returned 404 (not found) |
5 |
API returned 429 (rate limited — back off and retry) |
6 |
a remote system failed: API returned 5xx, or an upstream MCP connector failed |
7 |
mcp gateway call completed, but the tool itself reported an error (isError: true in its result) |
Pass --error-format json (or C1I_ERROR_FORMAT=json) to get a machine-readable
error object instead of the default Error: <msg> line. For API errors it
includes the status, method, path, and response body:
$ c1i api --path /api/v1/apps/<nonexistent-id> --error-format json
{"body":{"code":5,"message":"not found (request-id: ...)"},"error":"API error: API GET /api/v1/apps/<nonexistent-id> returned 404: ...","method":"GET","path":"/api/v1/apps/<nonexistent-id>","status":404}The body is embedded as JSON when the API returned JSON, otherwise as a string.
c1i requires a C1 URL. You can pass a full URL, a raw domain, or a legacy short tenant name. Set it via (in order of precedence):
--urlflagC1I_URLenvironment variable~/.c1i.yamlconfig file:url: https://mycompany.conductor.one
All of these are equivalent:
--url https://mycompany.conductor.one--url mycompany.conductor.one--url mycompany
For credential storage, see Credential sources below.
Transient API failures are retried automatically with exponential backoff and
jitter, honoring a Retry-After header when the server sends one. This keeps
long auto-paginated pulls from failing on a single rate-limit blip. What gets
retried depends on the request, to avoid duplicating side effects:
429 Too Many Requests— retried for every command (the request is rejected before the server processes it, so a retry is always safe).- Transient
5xx(500, 502, 503, 504) and network errors — retried only for idempotent reads and updates (GET/PUT/DELETE). Non-idempotentPOSTmutations (e.g.requests create,tasks approve) are not retried on these, since the server may have already applied the change before the failure. Non-transient 5xx (501 Not Implemented, 505, 511) are never retried.
Control the retry budget (attempts after the first try) via, in order of precedence:
--max-retries Nflag (applies to any command)C1I_MAX_RETRIESenvironment variable- Default:
4
Set --max-retries 0 to disable retries entirely. Non-retryable responses
(4xx other than 429, and 501/505) fail immediately.
--dry-run (or C1I_DRY_RUN=1) previews a mutating request — its method, path,
and pretty-printed JSON body — and returns without sending it:
$ c1i requests create grant --app-id A1 --entitlement-id E1 --user-id U1 --dry-run
[dry-run] POST /api/v1/task/grant
{
"appEntitlementId": "E1",
"appId": "A1",
"identityUserId": "U1"
}It applies to every write command (requests create, tasks approve/deny/comment,
accounts set-owner, the mcp mutations) and to non-GET api calls, and never
sends the mutation itself. Most previews run fully offline — no credentials
required. The exceptions are tasks approve/deny (authenticate and read the
task to resolve its current policy step) and requests create grant/revoke
when --user-id is omitted (authenticate to resolve it to the caller) — both so
the previewed body is exact.
--debug (or C1I_DEBUG=1) traces each HTTP request to stderr — method, URL,
response status, and elapsed time, including every retry attempt. Headers and
bodies are never logged, so credentials don't leak. Output goes to stderr, so it
won't corrupt piped JSON on stdout:
$ c1i apps list --debug 2>trace.log
$ cat trace.log
> GET https://mycompany.conductor.one/api/v1/apps
< GET /api/v1/apps 200 OK (142ms)# Browser-based login (OAuth device flow)
c1i auth login
# Or store credentials directly
c1i auth login --client-id <id> --client-secret <secret>
# Check credential status (also reports the storage backend)
c1i auth status
# Show the authenticated principal: user ID, display name, email, role/permission/feature counts
c1i auth whoami # add --verbose for full roles/permissions/features arrays
# Remove stored credentials
c1i auth logoutc1i reads credentials from the first source that has them, in this order:
- Environment variables — set
C1I_CLIENT_IDandC1I_CLIENT_SECRET(alongsideC1I_URL) for non-interactive / CI use. Both must be set; if only one is set the value is ignored. - OS keyring — Keychain on macOS, Credential Manager on Windows, Secret Service (e.g. gnome-keyring, KeePassXC) on Linux. Used by default when available.
- File fallback — a
0600JSON file under your config directory (~/.config/c1i/credentials/on Linux,~/Library/Application Support/c1i/credentials/on macOS,%AppData%\c1i\credentials\on Windows). Used automatically when no OS keyring is available — typical on headless Linux servers, containers, CI runners, and WSL without a desktop environment.
c1i auth login writes to the OS keyring when it can and falls back to the
file backend transparently. c1i auth status tells you which source served
the active credentials.
# bash
c1i completion bash > /etc/bash_completion.d/c1i
# zsh
c1i completion zsh > "${fpath[1]}/_c1i"
# fish
c1i completion fish > ~/.config/fish/completions/c1i.fishc1i version # or: c1i --versionApache 2.0