C1 Interface — and it looks like cli. Get it?
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.
Download signed macOS, Linux, and Windows releases with checksums, provenance, and SBOM attestations from the official distribution center.
# Homebrew
brew install conductorone/baton/c1i
# Go
go install github.com/ConductorOne/c1i@latest
# Container (image tags omit the leading "v" -- e.g. 0.5.2, not v0.5.2)
docker pull public.ecr.aws/conductorone/c1i:<version># 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 apps create --display-name <name> [--description <text>]
c1i apps owners <app-id> [--page-size N] [--page-token TOKEN] [--limit N]
c1i apps add-owner <user-id> --app-id <id>
c1i apps remove-owner <user-id> --app-id <id>
c1i apps set-owners <app-id> --user-id <id> [--user-id <id> ...] [--wait] [--wait-timeout 4m]
c1i apps delete <app-id>apps create needs only --display-name; it makes a plain, unmanaged
container app — the zero state for "make an app, then register MCP servers
under it". The caller is auto-assigned as an owner, showing up in apps owners
after the usual provisioning lag. The new app comes back as pretty JSON under
an app key, and --fields is never applied to mutation output, so read the
new id from .app.id on the full object, not .id.
apps delete is a soft delete: the app is marked with deletedAt rather than
erased, which is why apps list rows carry a deleted_at field. Both commands
honor --dry-run.
apps set-owners returns as soon as the PUT is accepted. Pass --wait to
block and poll GET .../ownerids until every requested --user-id appears, or
--wait-timeout (default 4m) elapses. A timeout exits 1 even though the
write itself was accepted — provisioning can simply still be in flight, so
re-check with apps owners rather than re-issuing the write. With --dry-run
the preview still only covers the PUT; --wait never polls.
apps owners is the read that reflects what apps add-owner,
apps remove-owner and apps set-owners write, and lags a write by roughly
45-150s (owner provisioning is asynchronous). It returns zero rows at exit 0
for a well-formed but nonexistent app id, so an empty result can also mean
"wrong id". apps add-owner and apps remove-owner change one owner at a
time; two add-owner calls issued at once were both observed to land.
apps set-owners replaces the whole list with exactly the ids you pass, so an
owner added between your read and your write is silently removed.
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>
# Directional entitlement-to-entitlement proxy binding
c1i entitlements proxy-bindings get \
--source-app-id <id> --source-entitlement-id <id> \
--destination-app-id <id> --destination-entitlement-id <id>
c1i entitlements proxy-bindings create \
--source-app-id <id> --source-entitlement-id <id> \
--destination-app-id <id> --destination-entitlement-id <id>
c1i entitlements proxy-bindings delete \
--source-app-id <id> --source-entitlement-id <id> \
--destination-app-id <id> --destination-entitlement-id <id>
# Create one on a manually-managed app, with its resource type and resource
c1i entitlements create --app-id <id> --display-name "Payroll admin" \
[--description <text>] [--slug member] [--alias payroll_admin] [--owner-id <user-id>] \
[--duration-grant 3600s] [--resource-type CUSTOM] \
[--resource-type-display-name "Payroll role"] [--resource-display-name "Payroll admins"]
# Reuse an existing resource type (or an existing resource) instead of creating one
c1i entitlements create --app-id <id> --display-name "Payroll viewer" --resource-type-id <id>
c1i entitlements create --app-id <id> --display-name "Payroll viewer (RO)" \
--resource-type-id <id> --resource-id <id>An entitlement points at an app resource, which lives under an app resource
type, so entitlements create is up to three POSTs: the resource type, the
resource, then the entitlement. --resource-type-id and --resource-id skip
whichever of the first two steps you already have — one resource type can carry
many resources, and one resource many entitlements. The server requires both
ids on the entitlement even though the OpenAPI schema marks only displayName
required, so --resource-id without --resource-type-id is rejected at exit
2 before anything is sent.
--resource-type is ROLE, GROUP, LICENSE, PROJECT, CATALOG,
CUSTOM, VAULT or PROFILE_TYPE (case-insensitive here, uppercase on the
wire; defaults to CUSTOM) and describes the resource type this command creates, so passing it
together with --resource-type-id is a usage error rather than a silently
ignored flag. Only CUSTOM can repeat on one app: a second resource type of
any other kind fails with a 500 (exit 6, though retrying never helps) saying
app resource type already exists, so reuse the existing one with
--resource-type-id and drop both --resource-type and
--resource-type-display-name — either one alongside the id is a usage error.
Reusing a resource with --resource-id likewise means you drop
--resource-display-name. --owner-id is repeatable and goes inline in the create
request, so no follow-up call is needed; an empty one is a usage error rather
than an owner quietly dropped. --duration-grant takes a protobuf duration —
seconds with an s suffix, e.g. 3600s; a Go-style 1h is refused by the
server. Omit it for standing access.
--dry-run previews all three requests, printing
NEW_APP_RESOURCE_TYPE_ID/NEW_APP_RESOURCE_ID where an id only exists after
a real preceding step. There is no rollback: if a later step fails, the objects
the earlier ones created still exist, and the error names them along with the
flags that reuse them and the create-only flags the retry has to drop. The
created entitlement comes back as pretty JSON under appEntitlementView
(--fields is never applied to mutation output); it echoes
appResourceTypeId/appResourceId and expands both objects, so every id the
command touched is in that one payload.
entitlements proxy-bindings manages directional entitlement-to-entitlement
visibility and tracking links. Source and destination are each identified by
their app and entitlement IDs; entitlement IDs are app-scoped. Creating a
binding does not grant access or configure delegated provisioning. Use
c1i docs guide delegate-entitlement-provisioning for the separate ordered
workflow that configures delegation on the destination entitlement. The public
REST API has no proxy-binding list endpoint; the C1 Console's binding browser
uses an internal gRPC-web search service that is outside the public API
contract.
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 app
# After a grant, wait for the set to stop changing rather than polling by hand
c1i grants list --app-id <id> --entitlement-id <id> --wait --wait-min 1Grants 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.
Grant provisioning is asynchronous, so a read taken moments after a grant or
revoke can catch the set mid-change. --wait re-reads every page every 5s and
prints nothing until the same grants come back --wait-stable times running
(default 3, minimum 2 -- one read cannot show that anything held steady).
Progress goes to stderr, so stdout stays pure NDJSON. The 5s interval is fixed
and is not a flag; --wait-stable must fit inside --wait-timeout (default
4m), and a combination that cannot fit is rejected as a usage error rather
than left to time out.
An empty result is stable. A filter matching nothing settles at the first
opportunity -- about 10s at the defaults -- and exits 0 with zero rows.
Waiting on a grant you just made, that reads as "it did not happen" when the
truth is "not yet". Pass --wait-min 1 (or the count you expect) to hold out
for that many grants and time out instead; exit 1 then means "did not
converge in time", not "absent". The default of 0 is deliberate:
empty-and-stable is the correct answer when you are waiting for a revoke --
and in that direction there is no flag that helps, since --wait-min is a
floor and cannot express a ceiling. --wait settles on whatever is steady, so
exit 0 still listing the row means "not yet, re-run", not "the revoke
failed".
--wait settles on the whole matching set, fetching every page on every poll
regardless of --limit; --limit only truncates what is printed. Filter
narrowly. --wait and --page-token are mutually exclusive.
A grant outlives the entitlement or account it points at, so rows also carry
entitlement_deleted_at and app_user_deleted_at: jq 'select(.entitlement_deleted_at)' finds grants whose backing object is gone.
Absent timestamps are null, never "".
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>
c1i tasks close <task-id> [--comment <text>]
c1i tasks reassign <task-id> --to-user-id <id> [--to-user-id <id> ...] [--policy-step-id <id>] [--comment <text>]
c1i tasks restart <task-id> [--policy-step-id <id>] [--comment <text>]
c1i tasks reset <task-id> [--comment <text>]
c1i tasks skip-step <task-id> [--policy-step-id <id>] [--comment <text>]
c1i tasks process <task-id>
c1i tasks update-grant-duration <task-id> --duration <duration>restart, reset and skip-step each rotate the task's current policy step,
so a --policy-step-id captured before one of them goes stale — the server
answers this action is no longer available: the request has advanced to a new approval step. Omit the flag to act on whatever step is current.
Which actions a task accepts depends on its state; the server refuses the rest
with action not permitted. Read the task's own list with
c1i api --path /api/v1/tasks/<task-id> --fields actions.
restart re-runs the current approval step; reset restarts the whole policy.
Neither reopens a closed task. process changes nothing observable on a
healthy task — it is intended for one that has stalled, which was not
reproduced here. update-grant-duration takes a
protobuf duration (3600s, not 1h) and only applies before the task reaches
provisioning, after which the server answers cannot update grant duration for a ticket in a provision step; the value lands as grantDuration.
escalate, update-request-data and approve-with-step-up are not wrapped;
reach them through c1i api.
approve/deny/reassign target a specific policy step. If --policy-step-id
is omitted, the task's currently executing step is fetched and used
automatically for all three — but approve and reassign require a resolvable
step and error if they can't find one, while deny proceeds without a step if
none can be derived.
close cancels a task without recording an approval decision, and takes no
step. Closing an already-closed task is rejected by the API with task is closed (exit 2).
reassign sets the step's approvers to the users named by
--to-user-id; repeat the flag to assign several.
close and reassign never print a task state: close reports task_id,
and reassign also reports the policy_step_id it acted on. The task action
endpoints return the task as it was before the action, so closing an open
task answers TASK_STATE_OPEN — echoing that would report the opposite of what
happened. Read the task back if you need the post-action state.
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> [--page-size N] [--page-token TOKEN] [--limit N]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. Fetched source is developer-authored code that commonly inlines credentials, so files are written 0600 and the directory is created — or, if it already exists, tightened, never widened — to at most 0700 (owner-only; group access to the directory would buy nothing since the files inside are already unreadable to group/other, and a filename alone can be informative). Any setuid/setgid/sticky bit on the directory is stripped outright, since none of them mean anything once there is no group or other access left. A tightened pre-existing directory prints a warning to stderr naming its old mode. 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.
c1i policies list [--page-size N] [--page-token TOKEN] [--limit N]
c1i policies get <policy-id>
c1i policies search [--query <text>] [--display-name <name>] [--policy-type grant|revoke|certify ...] [--include-deleted] [--exclude-policy-id <id> ...] [--page-size N] [--page-token TOKEN] [--limit N]
c1i policies create --display-name <name> --policy-type grant|revoke|certify [--description <text>] [--steps-file <file|-> ] [--rules-file <file|-> ] [--allow-deny-all]
c1i policies create --body-file <file|->
c1i policies update <policy-id> [--display-name <name>] [--description <text>] [--policy-type ...] [--steps-file <file|-> ] [--rules-file <file|-> ] [--allow-deny-all]
c1i policies update <policy-id> --body-file <file|-> --update-mask <paths>
c1i policies delete <policy-id>
c1i policies validate-cel <condition>A policy describes how C1 processes a task: who approves it (an ordered list
of policySteps, each a oneof of approval/provision/accept/reject/wait/form —
the schema also declares action, which the server rejects as an unsupported
step type — and an approval step's approver is itself a oneof of ten arms:
users, manager, group, appOwners, self, entitlementOwners, expression, webhook,
resourceOwners, agent), and how rules[] route a task to one of several
step sequences by CEL condition. That structure is too deeply nested for a
flag surface, so create/update take it from a JSON file (or - for
stdin) via --steps-file/--rules-file/--body-file — the same pattern
mcp servers register uses for its auth config.
Client-side guards (create and update, before any request is sent,
exit code 2) exist because several C1 policy API defects are either silent
or return an opaque HTTP 500 instead of a 400:
- Empty/missing steps for a policy's baseline entry are refused —
POST /api/v1/policieswith nopolicyStepssucceeds and silently returns a deny-everything policy (a single{"reject":{}}step), with no validation error. Pass--allow-deny-allif that's genuinely what you want (an explicitsteps:[]is refused regardless — the server 500s on that, not a safe default). --policy-typeunspecified, an emptyrules[].condition(needs the literal"true"for a baseline/catch-all rule), aprovisionstep,fallback/fallbackUserIdson an approver arm that doesn't support them (onlyusers,appOwners,webhook, andagentlack it — the other six arms each support their ownfallback/fallbackUserIds/fallbackGroupIds/isGroupFallbackEnabled), andfallback:truewith nothing to fall back to (a bare server error that surfaces asHTTP 500).
update sends the API's required {"policy": {...}, "updateMask": "..."}
wrapper for you — a flat body 400s. The --steps-file/--rules-file
convenience flags derive the update mask from what you pass; --body-file
requires an explicit --update-mask.
validate-cel checks a CEL condition without creating or updating anything.
Its root variable is subject (not user); this validates the
rules[].condition environment specifically, which is NOT the same
environment ExpressionApproval.expressions run in (see the command's
--help). An invalid condition prints its compile markers and exits 2, so
c1i policies validate-cel '<cond>' && ... only continues on a condition
that compiles.
A soft-deleted policy still returns from a direct get (with deletedAt
populated) but disappears from list and the default search; only
search --include-deleted finds it there. List and search rows carry
deleted_at so deleted rows are distinguishable without a second call — it
is null on a live policy, so jq 'select(.deleted_at)' selects only the
deleted ones.
list/search rows also carry step_kinds (the policy's baseline sequence in
run order, e.g. ["accept"]) and baseline_policy_id — the portable way to
identify a tenant's auto-approval grant policy, since display names vary
("Auto-approval" vs "Auto approval"). Match on the shape, not the name —
pipe c1i policies list into
jq -c 'select(.policy_type=="POLICY_TYPE_GRANT" and .system_builtin and .step_kinds==["accept"] and .baseline_policy_id==null)'.
A tenant can hold more than one such policy, so check what comes back rather
than taking the first row (rule_count > 0 means conditional rules route to
alternative sequences). A non-null baseline_policy_id means the policy defers
its baseline to another — step_kinds is then [] for a healthy policy.
policies get is a verbatim API passthrough and exposes neither derived key;
use list/search for them.
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> [--source-app-id <id>] [--auth ... ] [--config-field k=v ...] [--user-id <id> ...]
c1i mcp servers register --app-id <id> --type external --display-name <name> --server-url <url> [--transport streamable-http|sse] [--auth ...] [--user-id <id> ...]
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 (--server-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> # not paginated; returns the full set in one response
# 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> ...] # --tool-id max 100
c1i mcp bindings delete --app-id <id> --connector-id <id> --toolset-id <tid> --tool-id <id> [--tool-id <id> ...] # --tool-id max 100
c1i mcp bindings by-tools --app-id <id> --connector-id <id> --tool-id <id> [--tool-id <id> ...] # --tool-id max 32
c1i mcp bindings history --app-id <id> --connector-id <id> (--toolset-id <tid> | --tool-id <id>) [--page-size N] [--limit N]
# Classifiers (AI governance for MCP gateways)
c1i mcp classifiers list [--page-size N] [--page-token TOKEN] [--limit N]
c1i mcp classifiers get <classifier-id>
c1i mcp classifiers create --body-file classifier.json
c1i mcp classifiers update <classifier-id> --body-file classifier.json --update-mask <camelCase-fields>
c1i mcp classifiers delete <classifier-id>
c1i mcp classifiers bindings list [--page-size N] [--page-token TOKEN] [--limit N]
c1i mcp classifiers bindings create --body-file binding.json
c1i mcp classifiers bindings delete <binding-id>
c1i mcp classifiers templates list [--latest-only] [--page-size N] [--page-token TOKEN] [--limit N]
c1i mcp classifiers templates get <template-id> [--template-version <version>]
c1i mcp classifiers templates instantiate <template-id> [--body-file params.json]
c1i mcp classifiers templates add-rule <classifier-id> <template-id> [--body-file params.json]
c1i mcp classifiers policy show
c1i mcp classifiers policy update --body-file policy.json --update-mask <camelCase-fields>
c1i mcp classifiers tool-gates list [--page-size N] [--page-token TOKEN] [--limit N]
c1i mcp classifiers tool-gates search [--query <text>] [--page-size N] [--page-token TOKEN] [--limit N]
c1i mcp classifiers tool-gates get <tool-gate-id>
c1i mcp classifiers tool-gates create --body-file tool-gate.json
c1i mcp classifiers tool-gates update <tool-gate-id> --body-file tool-gate.json --update-mask <camelCase-fields>
c1i mcp classifiers tool-gates delete <tool-gate-id>
# 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>]mcp classifiers manages the AI-governance API introduced for MCP
gateways. Named classifiers are reusable ordered rule cascades; bindings attach
one to an AGENT or GATEWAY target. policy is the separate, tenant-wide
agent policy. A rule with an empty celCondition matches every tool call, and
rules execute in order, so read the current object before changing rules.
create accepts a resource JSON object: Classifier for classifiers,
ClassifierBinding for bindings, and the full create request for tool gates.
classifiers update must include the classifier's required displayName,
even when the mask changes another field; read, edit, and resubmit the current
object. Every update requires --update-mask; include rules only when
deliberately replacing the complete cascade. templates get shows required
parameter keys. instantiate and add-rule accept an optional
JSON object with templateVersion, params, and, for add-rule, ruleIndex
or insertIndex; the positional IDs always select the template and
classifier. A tool gate's filter must select either builtInPattern or
celExpression, not both.
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. --token-sharing shared|per-user sets the server's token-sharing mode (case-insensitive; per_user/peruser are also accepted). Per the register help, per-user is only valid with oauth2 in authorization-code or passthrough mode, bearerToken, customHeader, or basicAuth. Note that a read-back can legitimately differ from what you sent: the backend may store a resolved OAuth2 grant such as ..._MODE_AUTHORIZATION_CODE in place of the input mode, so that is a normal round-trip, not a bug. --source-app-id names the source app for a connector-backed HOSTED server. --data-sensitivity, --tool-prefix, --require-tool-approval and --user-id (repeatable — sets the connector's integration owners) can all be set at register time, not only via update.
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 them for the gateway to proxy calls. It takes one or more tool ids — the API has no batch approve, so each id is a separate request, but one invocation covers a whole toolset (pipe mcp tools search --app-id <id> --connector-id <id> --state pending --fields id | jq -r .id). 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 create grant --duration is a Go-style duration (24h, 7d) — unlike
entitlements create --duration-grant and tasks update-grant-duration --duration,
which take a protobuf duration (3600s, not 1h).
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.
findings covers the public REST finding, governance-rule, settings, audit,
and shadow-MCP occurrence surfaces.
c1i findings search [--body-file <file|->] [--page-size N] [--page-token TOKEN] [--limit N]
c1i findings get <finding-id>
c1i findings create --body-file <file|->
c1i findings state <finding-id> --body-file <file|->
c1i findings assignee <finding-id> --body-file <file|->
c1i findings create-task <finding-id> [--policy-id <policy-id>]
c1i findings bulk-state --body-file <file|->
c1i findings bulk-create-tasks --body-file <file|->
c1i findings routing-rules list [--limit N]
c1i findings routing-rules get <rule-id>
c1i findings routing-rules create --body-file <file|->
c1i findings routing-rules update <rule-id> --body-file <file|->
c1i findings routing-rules delete <rule-id>
c1i findings transformation-rules list [--limit N]
c1i findings transformation-rules get <rule-id>
c1i findings transformation-rules create --body-file <file|->
c1i findings transformation-rules update <rule-id> --body-file <file|->
c1i findings transformation-rules delete <rule-id>
c1i findings settings show
c1i findings settings update --body-file <file|->
c1i findings audits search [--body-file <file|->] [--page-size N] [--page-token TOKEN] [--limit N]
c1i findings shadow-mcp-occurrences search --body-file <file|-> [--page-size N] [--page-token TOKEN] [--limit N]The rule-list endpoints do not currently expose public filtering or paging; use
--limit only to cap local output. The upstream mapping gap is tracked in
#131. Use JSON files for oneof actions, custom
finding targets, and nested rules; the commands insert positional IDs into the
request. findings create-task accepts an optional --policy-id; C1 otherwise
selects the app policy or built-in Finding Review policy. bulk-state and
bulk-create-tasks are asynchronous and return a bulkActionId; prefer
explicit refs in the JSON to avoid unexpectedly applying a broad
searchRequest.
The public API intentionally excludes its internal summary, app-NHI, and
shadow-MCP notification RPCs. FindingSearchService.GetSearchTerms is also not
routed through public REST; it is tracked in #130.
role-mining manages the public role-mining analysis workflow, its suggested
access profiles, and tenant configuration.
c1i role-mining trigger
c1i role-mining suggestions list [--limit N]
c1i role-mining suggestions search --body-file <file|-> [--page-size N] [--page-token TOKEN] [--limit N]
c1i role-mining suggestions get <suggestion-id>
c1i role-mining suggestions state <suggestion-id> --body-file <file|->
c1i role-mining suggestions users <suggestion-id> --body-file <file|-> [--page-size N] [--page-token TOKEN] [--limit N]
c1i role-mining config show
c1i role-mining config update --body-file <file|->
c1i role-mining runs list [--limit N]
c1i role-mining runs latest
c1i role-mining custom-analysis list [--limit N]
c1i role-mining custom-analysis latest
c1i role-mining custom-analysis get <analysis-id>
c1i role-mining custom-analysis trigger --body-file <file|->
c1i role-mining custom-analysis evaluate <analysis-id> --body-file <file|->
c1i role-mining access-profiles create --body-file <file|->trigger queues an organization analysis and returns an enqueue acknowledgement;
inspect runs latest for its eventual result. custom-analysis trigger returns
an analysis ID that custom-analysis get can poll. The latest custom-analysis
pointer is scoped to the authenticated user; custom-analysis list and get are
tenant-scoped. config update replaces the entire configuration, so begin with
config show. Access-profile creation persists the profile, entitlement bindings,
and optional automation as separate service operations; validate its JSON before
submitting it.
The suggestions, runs, and custom-analysis list routes do not accept paging or
state filters through public REST, despite returning a continuation token.
--limit only caps local output; the upstream mapping gap is tracked in
#133. Organization overview,
legacy cohort analysis, coverage streaming, and the core MCP role-mining
service are not public REST routes.
Campaigns certify whether existing access should continue. The API calls the
resource an access_review, but c1i uses the UI's access-reviews name.
c1i access-reviews list [--page-size N] [--page-token TOKEN] [--limit N]
c1i access-reviews get <campaign-id>
c1i access-reviews create --body-file <file|->
c1i access-reviews update <campaign-id> --body-file <file|-> --update-mask <paths>
c1i access-reviews reports list <campaign-id> [--page-size N] [--page-token TOKEN] [--limit N]
c1i access-reviews reports generate <campaign-id> [--format json|csv|xlsx]
c1i access-reviews reports generate <campaign-id> --body-file <file|->create takes the complete API request object. Its scopeV2 is a deeply
nested oneof, so the CLI intentionally accepts it through --body-file rather
than a lossy flag surface; at least one ownerIds entry is required. update
takes a partial AccessReview object, inserts the id, and wraps it with the
required explicit --update-mask. Prepared campaigns cannot change their
scope or policy.
Report generation is asynchronous: generate creates a report record but does
not return a ready download. Use reports list to find its state and
time-limited downloadUrl. Public API callers default to JSON when --format
is omitted; use csv or xlsx when needed. --body-file is for the full
generation request, including XLSX report-column configuration; it is mutually
exclusive with --format.
An access profile controls which entitlements are requestable and who can request them — admins use them to grant birthright access or to open access up to a chosen audience.
The API calls this object a request catalog, and every path is
/api/v1/catalogs, so its JSON keys and ids say "catalog". The spec carries
both names — its RequestCatalog schema is tagged
x-speakeasy-entity: Access_Profile — so search for either.
Not to be confused with an app catalog, which is the per-user list of what one user can request, derived from the access profiles they belong to.
c1i access-profiles list [--page-size N] [--page-token TOKEN] [--limit N]
c1i access-profiles get <access-profile-id>
c1i access-profiles create --display-name <name> [--description <text>] [--published] [--visible-to-everyone] [--request-bundle] [--enrollment-behavior bypass|enforce] [--unenrollment-behavior leave-access-as-is|revoke-all|revoke-unjustified] [--unenrollment-entitlement-behavior bypass|enforce]
c1i access-profiles update <access-profile-id> [--display-name <name>] [--description <text>] [--published] [--visible-to-everyone] [--request-bundle] [--enrollment-behavior bypass|enforce] [--unenrollment-behavior leave-access-as-is|revoke-all|revoke-unjustified] [--unenrollment-entitlement-behavior bypass|enforce]
c1i access-profiles delete <access-profile-id>
c1i access-profiles requestable-entitlements list <access-profile-id> [--page-size N] [--page-token TOKEN] [--limit N]
c1i access-profiles requestable-entitlements list-ids <access-profile-id>
c1i access-profiles requestable-entitlements add <access-profile-id> --app-id <id> --entitlement-id <id> [--entitlement-id <id> ...] [--create-requests]
c1i access-profiles requestable-entitlements remove <access-profile-id> --app-id <id> --entitlement-id <id> [--entitlement-id <id> ...]
c1i access-profiles requestable-entitlements set <access-profile-id> --refs-file <path>
c1i access-profiles visibility-entitlements list <access-profile-id> [--page-size N] [--page-token TOKEN] [--limit N]
c1i access-profiles visibility-entitlements add <access-profile-id> --app-id <id> --entitlement-id <id> [--entitlement-id <id> ...]
c1i access-profiles visibility-entitlements remove <access-profile-id> --app-id <id> --entitlement-id <id> [--entitlement-id <id> ...]
c1i access-profiles bundle-automation get <access-profile-id>
c1i access-profiles bundle-automation create <access-profile-id> --body-file <path>
c1i access-profiles bundle-automation set <access-profile-id> --body-file <path>
c1i access-profiles bundle-automation delete <access-profile-id>
c1i access-profiles bundle-automation resume <access-profile-id>
c1i access-profiles bundle-automation run <access-profile-id> [--refs-file <path>]access-profiles create needs only --display-name. Every other field is
omitted unless explicitly passed; explicit false still reaches the API.
update derives its field mask from the flags supplied, so an explicit
--description "" clears it while omitted fields remain untouched. Behavior
flags accept short values shown above or the full REQUEST_CATALOG_* API enum.
delete is a soft delete: the catalog leaves access-profiles list, while
access-profiles get still returns it with deletedAt populated.
requestable-entitlements manages what the profile grants. add and remove
scope each entitlement to one --app-id; repeat --entitlement-id for that
application. set takes a JSON array of {"appId":"…","id":"…"} via
--refs-file (or - for stdin) and replaces the profile's entire requestable
set; use [] to clear it. list-ids returns the API's compact {appId,id}
reference list. --create-requests asks the server to create requests for
profile members when adding entries.
visibility-entitlements controls who can see the profile. Visibility additions
require a catalog that is published but not visible to everyone. The API returns
catalog must be published to add an access entitlement for an unpublished
catalog and catalog is visible to everyone, cannot add access entitlements
when --visible-to-everyone is set.
bundle-automation drives automatic bundled-entitlement grants. create and
set take a JSON object via --body-file (or - for stdin). The CLI validates
and sends createTasks, disableCircuitBreaker, enabled, and entitlements,
which contains an entitlementRefs array of {"appId":"…","id":"…"} objects.
run --refs-file takes that entitlement reference array and limits a run to
those references.
access-profiles list rows do not carry a member count: the list endpoint reports
memberCount as 0 for every catalog while access-profiles get on the same id
reports a non-zero count, so the key is omitted from list rows. access-profiles get
also carries the catalog's accessEntitlements (its visibility bindings),
empty when there are none, which list rows omit.
A service principal (SPC) is a tenant-owned non-human identity. The principal is
just an identity; a client credential on it is what a caller authenticates
with (a client_id/secret pair), and a binding lets another subject — a
function, SSO application, AuthZEN server, or edge — act as the principal.
This API is not in the public OpenAPI spec, so these first-class commands are the
way to reach it (the raw c1i api escape hatch works too).
c1i service-principals list [--page-size <n>] [--page-token <token>] [--limit <n>]
c1i service-principals get <sp-id>
c1i service-principals create --display-name <name>
c1i service-principals update <sp-id> --display-name <name>
c1i service-principals delete <sp-id>service-principals is aliased to sp. create needs only --display-name;
the new principal comes back as pretty JSON under servicePrincipal, unwrapped
so its id is at the top level (read it from .id).
Delete is destructive: service-principals delete removes the principal
and every credential issued for it. Before deleting, inspect the principal with
service-principals get <sp-id>. Use --dry-run to preview the DELETE request
and target path; it does not verify that the target exists.
Credentials are how a service principal authenticates. The secret is returned once, at creation, and cannot be retrieved again — capture it then.
c1i service-principals credentials list <sp-id>
c1i service-principals credentials get <credential-id> --service-principal-id <sp-id>
c1i service-principals credentials create <sp-id> --display-name <name> [--expires <duration>] [--scoped-role <role-id>] [--allow-cidr <cidr>] [--require-dpop]
c1i service-principals credentials update <credential-id> --service-principal-id <sp-id> --display-name <name>
c1i service-principals credentials revoke <credential-id> --service-principal-id <sp-id>--expires takes a positive Go duration (e.g. 720h). The server accepts the
range (0s, 4320h] — up to 180 days — rejecting more with value must be inside range (0s, 4320h0m0s]. Fractional seconds are preserved.
--scoped-role and --allow-cidr are repeatable and restrict the credential to
those role ids / source CIDRs. Repeat either flag for multiple values. An empty
occurrence is a usage error before any request is sent. --require-dpop requires
DPoP proof-of-possession at token exchange. Only --display-name can be changed
after a credential is created.
Bindings (a draft API) name a subject that authenticates as the principal. Exactly one subject is required; the SSO, AuthZEN, and edge subjects are app-scoped, so they take both an id and an app id.
c1i service-principals bindings add --service-principal-id <sp-id> --function-id <fn-id>
c1i service-principals bindings add --service-principal-id <sp-id> --sso-application-id <id> --sso-app-id <app-id>
c1i service-principals bindings add --service-principal-id <sp-id> --authzen-server-id <id> --authzen-app-id <app-id>
c1i service-principals bindings add --service-principal-id <sp-id> --edge-id <id> --edge-app-id <app-id>
c1i service-principals bindings list --function-id <fn-id>
c1i service-principals bindings delete --service-principal-id <sp-id> --function-id <fn-id>bindings list is by subject — it returns the principals a subject is bound to.
add and delete are idempotent.
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 — and if that response carries a non-empty nextPageToken, c1i warns on stderr that the result is partial and names --paginate, since a truncated page is otherwise indistinguishable from a complete one at exit 0. stdout is unchanged either way.
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 search is a semantic search with no relevance threshold: every query returns up to 10 nearest matches, so even a nonsense query comes back with plausible-looking hits. A returned hit is not proof a concept exists, and an unexpected hit is not proof the thing you searched for is absent — read the snippet, or fetch the page with docs page, to judge. To check whether an API endpoint exists, use docs endpoints --filter, which has a real no-match.
docs 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). A200whose body is not JSON is an error (exit6), not a silent pass-through — a--paththat escapes the API prefix can reach the web app and return HTML, which used to print as though it had succeeded. An empty body still succeeds, since some endpoints answer a write with nothing.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-sizerequests a per-call batch size (max 100;mcp tools historyandmcp bindings historyallow 200). It is not a guarantee: a page can contain more rows than you asked for, by an amount that varies per endpoint and per size —apps list --page-size 10returned 23 rows,policies list12,users listexactly 10. A positive value below 5 usually returns 5, thoughpolicies listfloors at 6 andmcp servers catalog listhas no floor.--page-size 0means the server's default of 25, not "none". A value over the max is clamped by c1i rather than rejected. A negative--page-sizeor--limitis a usage error (exit 2), rejected before any request. Use--limit Nfor an exact total: it is enforced client-side, so it holds even when a page overshoots, and it stops auto-pagination once reached.
A flag documented as repeatable takes one value per occurrence, and a comma is a literal character rather than a separator:
c1i mcp bindings create --app-id A --connector-id C --toolset-id T \
--tool-id tool-a --tool-id tool-b # two tools--tool-id tool-a,tool-b is one id containing a comma, not two ids. This is
easy to miss on --config-field, where --config-field "region=us1,env=prod"
sets region to us1,env=prod and the server may accept it.
An empty occurrence is a usage error (exit 2), rejected before any request, so
an unset shell variable cannot silently shorten a list or remove a credential
restriction. Contrast --fields, which is comma-separated.
--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 print the resource itself, so--fields idyields{"id": ...}andjq -r .idworks. The API wraps the resource under its own key (app,function,userView.user, ...);getunwraps that and keeps every other envelope key —expandedamong them — as a top-level sibling. Do not write the wrapper into a path:--fields function.idno longer resolves, because there is nofunctionkey left. c1i apiis a raw passthrough and still returns the envelope. There, and for any genuinely nested field, a name that doesn't match at the top level is also searched for deeper: the shallowest match wins, and a tie at the same depth resolves to the alphabetically first full path, deterministically.- Mutation output (
apps createreturns{"app": ...}) also keeps the envelope, but is never projected at all —--fieldscannot blank a success message, so no search happens there. - 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.- On list commands and
api --paginate, "the response" means the whole result, not each row: rows are decided and streamed out one at a time as they're fetched (never buffered — these commands can walk unbounded, multi-page results), but a row whose projection is empty is skipped rather than printed as{}— a field present on some rows but absent on others just means fewer rows are printed, not an error, as long as it matches at least one row anywhere. Only a spec that matches nothing in every row across every page is an error, with a message ("...matched no keys in any row of the response") distinguishable from the single-object case above. Because every row's own projection was already empty (and skipped) by the time that whole-result verdict is known, nothing is printed on stdout before the error — never rely on output length to detect success without checking the exit code. If you're piping tojq(c1i ... | jq ...), remember$?reads the pipe's last command, notc1i's — the stderr message is the durable signal here, not the exit code you'd read from a naive$?after the pipe. - Worst case: a
--fieldsspec that matches nothing at all, paired with--limit, scans the collection to completion before erroring — "nothing ever matched" can't be known short of exhausting every page, and a typo is the ordinary way to hit this. Live-measured:tasks list --fields <typo> --limit 2made 193 requests over ~41s before exiting2on a tenant with ~9,650 tasks; on a 35,000-rowentitlements listthat's minutes. This isn't specific to--fields— it's the same ruleaccounts list --unmapped-onlyandfunctions usagealready follow for their own client-side filtering (a filter applied after the fetch can't bound the work when nothing matches, no matter what--limitsays);--fieldscombined with--limitis one more instance of it, not a new behavior.
- On list commands and
- 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, an empty id, an id the API redirects to a collection, or the API returned any 4xx other than 401/403/404/408/429/499) |
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 |
C1 failed: the API returned 5xx, a redirect chain never settled, or it answered 200 with a body that isn't JSON |
7 |
mcp gateway call completed, but the tool itself reported an error (isError: true in its result) |
8 |
a system beyond C1, or the MCP protocol layer, failed — an upstream connector was unreachable or errored, the gateway itself was unreachable (DNS failure, refused connection), or the gateway returned a protocol-level JSON-RPC error |
6 and 8 are both "something remote broke," but they call for different
responses. 6 means C1 itself is failing, so retrying later is reasonable and
there is nothing to fix at your end. 8 means C1 answered fine and something
past it did not, so the same call will usually fail the same way: if a connector
is at fault, inspect it (c1i mcp servers get <connector-id> --app-id <id>, and
mcp servers test-connection for an EXTERNAL server) — it may be unreachable or
its credentials may have expired; if it's a protocol-level JSON-RPC error, that
indicates a version mismatch or a bug in c1i, worth reporting rather than
working around. Before this split both arrived as 6, which made "C1 is down"
indistinguishable from "the Slack connector is down."
An empty id argument is a usage error, not a lookup: c1i policies get ""
exits 2 and sends nothing. Without that guard an empty id renders a trailing
empty path segment, which the API redirects to the collection endpoint — so the
command used to print the entire list and exit 0. The same check applies to a
raw api --path ending in /.
Relatedly, the REST client is selective about HTTP redirects. It follows one
only when both hold: the target path is identical to what was requested (a
trailing-slash difference counts as a change), and the target host is in the same
trust scope as the request host — the same host differing only in scheme or port,
or a label.-prefix relationship in either direction with at least two labels,
which covers apex ↔ www canonicalization. Anything else — a different path, or
the same path on an unrelated host — is refused as an error (exit 2) naming the
target.
Both halves matter. The path rule is what closed a real bug: an id of / or .
escapes to a path the API redirects to the collection, which turned a
single-object read into a full listing with exit 0. The host rule is what keeps
your token safe: a followed redirect is re-authenticated, so an unrestricted
follow would hand your bearer token to whatever host the redirect named.
A chain of allowed redirects that doesn't settle within five hops fails as a
remote error (exit 6) rather than looping.
This applies to every command built on the shared transport: the REST client,
the MCP gateway, and the login handshake, so the path and redirect guards,
--debug tracing, and --max-retries cover the gateway and login too, not just
REST commands. None of those four applies to the docs subcommands that
fetch — docs search, docs page, docs openapi, docs endpoints,
docs endpoint — which call Go's default HTTP client directly: no path or
redirect guard there, and --debug and --max-retries are both inert.
One narrower carve-out inside login: the device-flow token poll forces its own
retry count to zero, because RFC 8628's polling interval already is that
call's retry strategy and a second layer underneath would double the delays.
--max-retries still governs the rest of the handshake.
A bad id is the only cause of a refused 3xx observed so far, which is why it
maps to exit 2 — a redirect on an otherwise well-formed request would not be
the caller's mistake, and would still report 2.
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.
c1i requires https. A URL with any other scheme is rejected (exit 2); it
is not rewritten. Credentials embedded in the URL are dropped with a warning on
stderr, and an embedded password is never echoed.
The scheme may be omitted (tenant.c1eu.ai), in which case https is assumed.
The host is lower-cased, so HTTPS://TENANT.C1EU.AI and tenant.c1eu.ai
resolve identically, and a protocol-relative //tenant.example is handled.
Both *.conductor.one and *.c1eu.ai (EU) tenant domains are accepted — pass
whichever your tenant uses. Only the shape of the URL is checked, never the
domain, so a typo like mycompany.conductor.on is accepted here and surfaces
later as an authentication failure.
A bare name (--url mycompany) is rejected: with more than one tenant domain in
use it is ambiguous, and silently expanding it to mycompany.conductor.one would
point an EU tenant at the wrong region. The error names where the value came
from, which matters when it is a stale entry in ~/.c1i.yaml rather than
something you just typed. A single-label host is fine as long as it arrives as a
URL (https://c1-staging), which is how an internal-resolver name is reached.
If you previously authenticated with a mixed-case
--url, your stored credential was keyed by that exact casing and is no longer found now that the host is normalized. Runc1i auth loginonce to re-store it.
Set it via (in order of precedence):
-
--urlflag -
C1I_URLenvironment variable -
~/.c1i.yamlconfig file:url: https://mycompany.conductor.one
These are equivalent:
--url https://mycompany.conductor.one--url mycompany.conductor.one--url MYCOMPANY.CONDUCTOR.ONE(the host is lower-cased)
Every command prints which tenant it's about to use when the URL came from
~/.c1i.yaml. Nothing on the command line names the config file, so a
stale entry there sends a command to the wrong tenant with no visible sign
otherwise. --url and C1I_URL don't print this warning: both are explicit
choices made for that invocation, and warning on every normal use of
C1I_URL would just train you to stop reading it. The warning goes to
stderr, once per invocation (never once per page of a paginated list):
Warning: no --url flag given; targeting https://mycompany.conductor.one (from ~/.c1i.yaml)
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 (any command that reaches the C1 API)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.
This covers the token mint too: minting or refreshing the OAuth2 bearer
c1i authenticates with is itself a request, subject to the same 429 retry
(never 5xx/network, since it's a POST) and the same --max-retries budget.
Every request also gets a fixed timeout, per attempt (a retried request
gets a fresh budget, not a shrinking share of one deadline): 10 minutes
for a REST or MCP gateway request, 30 seconds for auth login's
device-flow requests and the OAuth2 token mint/refresh above — tighter
because those are fast request/response exchanges, not the kind of call
that legitimately runs long. Neither is configurable. 10 minutes leaves
roughly 3x headroom over the longest request this CLI is known to make
(an MCP tools/call invoking a slow tool, observed at 182 seconds), so
there's no known case that needs a longer one.
--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/close/reassign, 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/reassign (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 API 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 (principle/user ID, role/permission/feature counts, and
# display name + email when a best-effort secondary lookup succeeds) plus the resolved
# tenant: "tenant" (base URL) and "tenantSource" (flag/env/config)
c1i auth whoami
# --verbose swaps the summary for the raw introspect payload (full roles/permissions/
# features arrays, but no display name or email) -- a different projection, not a superset
c1i auth whoami --verbose
# Machine-readable "which tenant am I about to write to?" — the pre-write check
c1i auth whoami --url https://mycompany.conductor.one --fields tenant
# Mint a short-lived bearer token for driving raw API calls yourself
c1i auth token # add --json for token type and absolute expiry (RFC3339)
# Remove stored credentials
c1i auth logoutc1i auth token prints just the access token, newline-terminated, so it
composes into curl -H "Authorization: Bearer $(c1i auth token)" .... It is
never written to disk — a new one is minted per invocation.
c1i 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.fishpowershell is also available. Each generator takes --no-descriptions to
emit a script that completes names only, without the per-command help text.
c1i version # or: c1i --versionApache 2.0