npx skills add ...
npx skills add arize-ai/phoenix --skill phoenix-cli
Debug LLM applications using the Phoenix CLI. Fetch traces, spans, and sessions, annotate them, analyze errors, inspect datasets, review experiments, query annotation configs, and use the GraphQL API. Use whenever the user works with a Phoenix instance from the terminal.
npx skills add arize-ai/phoenix --skill phoenix-cli
The CLI uses singular resource commands with subcommands like list and get:
Every delete above is gated: it requires
PHOENIX_CLI_DANGEROUSLY_ENABLE_DELETES=true in the environment and prompts for
confirmation unless -y/--yes is passed (px profile delete is local-only and
takes --yes without the env gate). Without the env var the command exits
without deleting anything.
PHOENIX_ENDPOINT is the base URL for API access. It usually holds the same URL as PHOENIX_COLLECTOR_ENDPOINT; when only the collector variable is set, the CLI uses it for API access too.
For interactive local use, px auth login stores an OAuth session in the selected profile; the session acts with the permissions of the user who logged in. API keys take precedence over OAuth tokens when both are configured.
OAuth access tokens are refreshed automatically for REST, GraphQL, and PXI
requests, and rotated tokens are persisted to the selected profile.
Always use --format raw --no-progress when piping to jq.
px setup — onboardingpx setup connects the app in the current directory to a Phoenix deployment
and writes .env.phoenix (mode 0600, gitignored). The interactive flow is for
humans — it prompts, launches coding agents, and polls for traces. From an
agent, always pass --no-input:
Headless requires a clean git repo and, by default, stops after writing the
files — it will not touch source unless you ask. If auth is enabled, also set
PHOENIX_API_KEY. The project doesn't need to exist — Phoenix creates it on
first trace. Missing inputs exit 3 with exact remediation; cancel exits 2.
To also instrument the app, name the lane — headless has no prompt to pick one
from, so --instrument requires --agent:
--yolo matters: a background agent has no terminal to approve its edits on,
so without it the run stalls until trace verification times out. --language python skips the agent's language detection. --docs-mcp connects the
Phoenix docs MCP server to the hand-off agent (claude mcp add for claude,
config-file merge for cursor/opencode; codex unsupported) and skips the
.px/docs download — the agent searches docs on demand instead; any failure
falls back to the download. --no-docs-mcp suppresses the interactive offer.
--format raw prints
{"endpoint","project","files","instrumentation","tracesVerified","tracesUrl"}
— check tracesVerified, which is set only when the API confirmed a trace
arriving, not when the agent claims it finished.
A run whose wait ran out with no trace exits 6, not 0: the configuration and
edits are real, but tracing is not confirmed working. Treat that as a failure to
report, not a success — and do not substitute the hand-off agent's own exit code
or summary for the verdict. Registering without --instrument, and a human
answering "verify later" at the timeout prompt, both exit 0.
tracesVerified is false for a registration-only run too, so it alone can't
tell "nothing to verify" from "no trace arrived". Read verification
(verified / notVerified / deferred, absent when there was nothing to
verify) when you need the difference.
Re-runnable slices, so an already-registered repo skips the questions:
px setup mcp — register the remote MCP serverWire the Phoenix remote MCP server (<endpoint>/mcp) into a coding agent so it
can query Phoenix data. The endpoint is inferred from --endpoint, the active
profile, or PHOENIX_ENDPOINT. Bare command prompts for scope (global default) then
agent; --agent skips both prompts.
Agents: claude, codex, gemini, cursor, opencode, vscode. Scope is
--global (default) or --local (repo; Codex is global-only). Auth is OAuth by
default (URL-only config, browser login on first use); pass --header "Name: value" (repeatable) for an API-key bearer fallback — for Codex a
Authorization: Bearer ${VAR} header becomes bearer_token_env_var. --format raw prints {"endpoint","url","serverName","agent","scope","auth","file?"}.
auth status reports the credential source (flag, env, profile-key, oauth, or none). OAuth status includes the token expiry.
When the stored credential source is oauth and the authenticated probe fails,
auth status retries once without credentials and reports anonymous access only
if the server explicitly says access is anonymous. This keeps a stale or expired
profile token from being reported as an auth failure against a deployment that
has since switched from OAuth to anonymous access.
Named profiles let you switch between multiple Phoenix instances (local, staging, cloud) without juggling environment variables. Profiles are stored in ~/.px/settings.json (or $XDG_CONFIG_HOME/px/settings.json).
Configuration priority (highest to lowest): CLI flags > env vars > active profile > nearest .env.phoenix file > built-in defaults.
The CLI also discovers the nearest .env.phoenix file at or above the current working directory (the same file px setup writes). Credentials are resolved as one group, so a process API key is never combined with file-provided headers. Set PHOENIX_DISCOVER_CONFIG=false to disable discovery.
Use --profile <name> on any command to target a specific profile without changing the active one:
px profile create options: --endpoint <url>, --project <name>, --api-key <key>, --header <key=value> (repeatable), --activate.
project list accepts --limit <n> (projects fetched per page) and
--name-contains <filter>, which filters server-side on a case-insensitive name
substring. Use it instead of piping list through grep when you only know part
of a project's name.
project get exits with ExitCode.FAILURE (1) on a name miss and writes a StructuredError {error, code: "FAILURE", hint} to stderr in --format json|raw.
px <entity>-annotations delete requires --all or both --start-time and --end-time and emits {deleted: true, target, filter} on success.
session list has no filter flag. To select sessions by shape — error counts,
token totals, tool use, annotation labels — use the session filter expression
language through GraphQL (see Session filter expressions).
Full CRUD: list, get, create, update, delete. Types are CATEGORICAL (labels + optional scores), CONTINUOUS (numeric range), FREEFORM (free text).
Categorical values are specified the same way in create and update: repeatable --value label[=score] (score optional), or a single --values '<json>' payload — mutually exclusive. update fetches the existing config, merges your flags, and writes the full body back via PUT /v1/annotation_configs/{id}; it requires at least one field flag. Other type-specific flags: --lower-bound/--upper-bound (CONTINUOUS/FREEFORM), --threshold (FREEFORM). Invalid input (bad flags, type mismatches, malformed values) exits 3 (INVALID_ARGUMENT) with a {error, code, hint?} JSON envelope on stderr in raw/json mode. get/create/update output the config object (single object in raw/json, not an array).
For ad-hoc queries not covered by the commands above. Output is {"data": {...}}.
Key root fields: projects, getProjectByName(name:), datasets, prompts, evaluators, projectCount, datasetCount, promptCount, evaluatorCount, viewer.
getProjectByName(name:) targets one project; projects(first: 1) picks an
arbitrary one. There is no traces connection: to list traces, query spans
with filterCondition: "parent_span is None", which keeps root spans, as the
UI's traces table does. See Filter expressions below.
spans, sessions, and the project aggregates take filter conditions: Python
boolean expressions compiled server-side. There are three languages, and the
argument picks the language. Read
references/filter-expressions.md before
writing a condition; it has the full vocabulary, operators, and compiled
examples for each.
| Argument | Matches | Names come from |
|---|---|---|
filterCondition | individual spans | the exhaustive table in the reference |
traceFilterCondition | whole traces | traceFilterVocabulary |
sessionFilterCondition | sessions | sessionFilterVocabulary |
Root spans. There is no traces connection and no root-span argument.
filterCondition: "parent_span is None" keeps root spans, including orphans
whose parent was never received, and is what the UI's traces table runs;
parent_id is None keeps only spans with no parent id. A root span is usually
one per trace, and either clause composes with the rest of the filter:
Annotations. The accessor picks the level, and the wrong level matches nothing:
| Accessor | Matches annotations on | Written by |
|---|---|---|
annotations["name"] | the span itself | px span annotate, px span add-note |
trace_annotations["name"] | the span's parent trace | px trace annotate, px trace add-note |
session_annotations["name"] | the session (session filter only) | px session annotate, px session add-note |
Traces. traceFilterCondition keeps the spans of matching traces and
composes with filterCondition:
Sessions. px session list has no filter flag, so selecting sessions by
shape goes through GraphQL:
Discover names and validate. The vocabularies are generated from the compiler's own bindings, so they always match what compiles:
On fields that accept both levels (e.g. Project.recordCount),
sessionFilterCondition and filterCondition are mutually exclusive.
Download Phoenix documentation markdown for local use by coding agents.
Key options: --workflow (repeatable, values: tracing, evaluation, datasets, prompts, integrations, sdk, self-hosting, all), --dry-run, --refresh, --output-dir (default .px/docs), --workers (default 10).