Generated banner for Phoenix CLI
Generated logo for Phoenix CLI

Phoenix CLI

GitHubOfficialCreated by arize-aiDistributed by GitHub

  • Skill
  • Tool integration
  • Software development
  • Apache-2.0
  • No issues found

From the author

Debug LLM applications using the Phoenix CLI. Fetch traces, analyze errors, structure trace review with open coding and axial coding, inspect datasets, review experiments, query annotation configs, and use the GraphQL API. Use whenever the user is analyzing traces or spans, investigating LLM/agent failures, deciding what to do after instrumenting an app, building failure taxonomies, choosing what evals to write, or asking "what's going wrong", "what kinds of mistakes", or "where do I focus" — even without naming a technique.
— the description by arize-ai

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The Phoenix CLI is a command-line tool designed for inspecting and debugging LLM-powered applications. It allows developers to fetch and analyze traces, spans, and sessions, facilitating the identification of failure modes. The tool supports structured workflows like open coding and axial coding to help developers build failure taxonomies, prioritize fixes, and design evaluation targets based on real-world application traffic.

What it does

  • Fetches and lists traces, spans, and sessions for LLM applications.
  • Supports annotation of artifacts to track observations and failure modes.
  • Enables structured qualitative analysis through open and axial coding workflows.
  • Provides access to datasets, experiments, and annotation configurations.
  • Allows custom data extraction and analysis via a GraphQL API.
  • Manages multiple connection profiles for local, staging, and production environments.

When to use it

  • When you need to investigate why an LLM agent or application is failing.
  • When you want to analyze LLM traffic to identify patterns in errors or performance.
  • When you are building a failure taxonomy to prioritize development fixes.
  • When you need to determine which evaluation metrics to implement for your application.
  • When you need to switch between different Phoenix project environments efficiently.

Advantages

  • Provides a standardized way to perform qualitative analysis on LLM outputs.
  • Supports complex filtering and data extraction using standard CLI tools like jq.
  • Enables consistent tracking of failure modes across different development runs.
  • Simplifies the transition from raw observation to structured evaluation targets.

Usage examples

“List the last 20 traces that resulted in an error.”
The CLI executes a trace list command filtered by status, outputting the results in a raw format suitable for further processing with jq.
“Add a note to trace ID 12345 indicating that the context was lost.”
The CLI records the note against the specified trace, optionally tagging it with a coding annotation identifier for later grouping.
“Switch to the staging profile and check the connection status.”
The CLI updates the active configuration to the staging profile and verifies connectivity to the configured endpoint.
“Fetch all spans for a specific trace ID.”
The CLI retrieves the span data for the given trace, allowing for granular inspection of individual operations within the call graph.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/github/phoenix-cli/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip phoenix-cli.zip -d .agents/skills

Documentation by arize-ai

Phoenix CLI

Invocation

px <resource> <action>                          # if installed globally
npx @arizeai/phoenix-cli <resource> <action>    # no install required

The CLI uses singular resource commands with subcommands like list and get:

px trace list
px trace get <trace-id>
px trace annotate <trace-id>
px trace add-note <trace-id>
px trace-annotations delete
px span list
px span annotate <span-id>
px span add-note <span-id>
px span-annotations delete
px session list
px session get <session-id>
px session annotate <session-id>
px session add-note <session-id>
px session-annotations delete
px dataset list
px dataset get <name>
px project list
px project get <name>
px annotation-config list
px auth status
px profile list
px profile show [name]
px profile create <name>
px profile use <name>
px profile edit <name>
px profile delete <name>

Setup

export PHOENIX_HOST=http://localhost:6006
export PHOENIX_PROJECT=my-project
export PHOENIX_API_KEY=your-api-key  # if auth is enabled

Always use --format raw --no-progress when piping to jq.

Quick Reference

TaskFiles
Look at sampled traces, spans, or sessions and write specific notes about what went wrong (no taxonomy yet)references/open-coding
Group those notes into a structured failure taxonomy and quantify what mattersreferences/axial-coding

Both stages tag every artifact with one shared coding annotation identifier (descriptive shape, e.g. coding-run:chatbot-context-loss-2026-05-06) so the run is queryable, reversible, and viewable as a unit. Pass --identifier <value> explicitly on every px call — shell inheritance is unreliable across agent harnesses. Open coding writes notes via px ... add-note and records a small local JSONL sidecar at .px/coding/<sanitized-identifier>.jsonl; axial coding reads that sidecar as the deterministic handoff and records labels in .px/coding/<sanitized-identifier>-axial.jsonl. Pick the identifier once per run (see references/open-coding.md), then share the Phoenix UI link from the wrap-up section. Revert is opt-in and runs three identifier-bound DELETEs only after explicit user confirmation.

Workflow term vs. server annotation name. The skill prose calls this value the coding annotation identifier (shell-variable hint: CODING_ANNOTATION_IDENTIFIER). The server-side annotation NAME used for the UI filter is unchanged — coding_session_id — for data compatibility with rows already written by previous runs. Don't try to rename the server-side annotation; treat the asymmetry as load-bearing.

Workflows

"What do I do after instrumenting?" / "Where do I focus?" / "What's going wrong?" open-coding → axial-coding → build evals for the top categories.

Reference Categories

PrefixDescription
references/open-codingFree-form notes against sampled traces, spans, or sessions — reach for it whenever the user wants to make sense of LLM traffic but has no failure categories yet. Includes a unit-of-analysis diagnostic so the workflow runs at the level the failure modes actually live at (trace for stateless single-shot calls, session for multi-turn agents, span for mechanical/in-isolation failures).
references/axial-codingInductive grouping of notes into a MECE taxonomy with counts — reach for it whenever the user has observations and needs categories or eval targets

Auth

px auth status                                # check connection and authentication
px auth status --endpoint http://other:6006   # check a specific endpoint
px auth status --profile staging              # check a named profile's connection

Profiles

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 > built-in defaults.

px profile list                              # list all profiles (shows active profile)
px profile show                              # show the active profile's settings
px profile show staging                      # show a named profile's settings
px profile create prod --endpoint https://app.phoenix.arize.com --api-key <key> --activate
px profile create local --endpoint http://localhost:6006 --project my-app
px profile use prod                          # switch the active profile
px profile edit prod                         # open profile JSON in $EDITOR (validates on save)
px profile delete prod --yes                 # delete a profile (--yes skips confirmation)

Use --profile <name> on any command to target a specific profile without changing the active one:

px trace list --profile staging --limit 10 --format raw --no-progress | jq .
px auth status --profile prod

px profile create options: --endpoint <url>, --project <name>, --api-key <key>, --header <key=value> (repeatable), --activate.

Projects

px project list                                            # list all projects (table view)
px project list --format raw --no-progress | jq '.[].name' # project names as JSON
px project get my-project --format raw --no-progress       # single record by exact name
px project get my-project --format raw --no-progress | jq -r '.id'  # extract project id

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.

Traces

px trace list --limit 20 --format raw --no-progress | jq .
px trace list --last-n-minutes 60 --limit 20 --format raw --no-progress | jq '.[] | select(.status == "ERROR")'
px trace list --since 2025-01-15T00:00:00Z --limit 50 --format raw --no-progress | jq .
px trace list --format raw --no-progress | jq 'sort_by(-.duration) | .[0:5]'
px trace list --include-notes --format raw --no-progress | jq '.[].notes'
px trace get <trace-id> --format raw | jq .
px trace get <trace-id> --format raw | jq '.spans[] | select(.status_code != "OK")'
px trace get <trace-id> --include-notes --format raw | jq '.notes'
px trace annotate <trace-id> --name reviewer --label pass
px trace annotate <trace-id> --name reviewer --score 0.9 --format raw --no-progress
px trace annotate <trace-id> --name reviewer --label pass --identifier "<coding-annotation-id>"  # tag with a coding annotation identifier
px trace add-note <trace-id> --text "needs follow-up"
px trace add-note <trace-id> --text "needs follow-up" --identifier "<coding-annotation-id>"  # tag + upsert on identifier
px trace-annotations delete --identifier "<coding-annotation-id>" --all -y            # nuke every annotation tied to this coding annotation identifier

px <entity>-annotations delete requires --all or both --start-time and --end-time and emits {deleted: true, target, filter} on success.

Trace JSON shape
Trace
  traceId, status ("OK"|"ERROR"), duration (ms), startTime, endTime
  annotations[] (with --include-annotations, excludes note)
    name, result { score, label, explanation }
  notes[] (with --include-notes)
    name="note", result { explanation }
  rootSpan  — top-level span (parent_id: null)
  spans[]
    name, span_kind ("LLM"|"CHAIN"|"TOOL"|"RETRIEVER"|"EMBEDDING"|"AGENT"|"RERANKER"|"GUARDRAIL"|"EVALUATOR"|"UNKNOWN")
    status_code ("OK"|"ERROR"|"UNSET"), parent_id, context.span_id
    notes[] (with --include-notes)
      name="note", result { explanation }
    attributes
      input.value, output.value          — raw input/output
      llm.model_name, llm.provider
      llm.token_count.prompt/completion/total
      llm.token_count.prompt_details.cache_read
      llm.token_count.completion_details.reasoning
      llm.input_messages.{N}.message.role/content
      llm.output_messages.{N}.message.role/content
      llm.invocation_parameters          — JSON string (temperature, etc.)
      exception.message                  — set if span errored

Spans

px span list --limit 20                                    # recent spans (table view)
px span list --last-n-minutes 60 --limit 50                # spans from last hour
px span list --since 2025-01-15T00:00:00Z --limit 50       # spans since a timestamp
px span list --span-kind LLM --limit 10                    # only LLM spans
px span list --status-code ERROR --limit 20                # only errored spans
px span list --name chat_completion --limit 10             # filter by span name
px span list --trace-id <id> --format raw --no-progress | jq .   # all spans for a trace
px span list --parent-id null --limit 10                   # only root spans
px span list --parent-id <span-id> --limit 10              # only children of a span
px span list --include-annotations --limit 10              # include annotation scores
px span list --include-notes --limit 10                    # include span notes
px span list --attribute llm.model_name:gpt-4 --limit 10  # filter by string attribute
px span list --attribute llm.token_count.total:500 --limit 10  # filter by numeric attribute
px span list --attribute 'user.id:"12345"' --limit 10     # force string match for numeric-looking value
px span list --attribute session.id:sess:abc:123 --limit 20  # colon in value OK (split on first colon only)
px span list --attribute llm.model_name:gpt-4 --attribute session.id:abc --limit 10  # AND multiple filters
px span list output.json --limit 100                       # save to JSON file
px span list --format raw --no-progress | jq '.[] | select(.status_code == "ERROR")'
px span annotate <span-id> --name reviewer --label pass
px span annotate <span-id> --name checker --score 1 --annotator-kind CODE
px span annotate <span-id> --name reviewer --label pass --identifier "<coding-annotation-id>"  # tag with a coding annotation identifier
px span add-note <span-id> --text "verified by agent"
px span add-note <span-id> --text "verified by agent" --identifier "<coding-annotation-id>"  # tag + upsert on identifier
px span-annotations delete --identifier "<coding-annotation-id>" --all -y           # nuke every annotation tied to this coding annotation identifier
Span JSON shape
Span
  name, span_kind ("LLM"|"CHAIN"|"TOOL"|"RETRIEVER"|"EMBEDDING"|"AGENT"|"RERANKER"|"GUARDRAIL"|"EVALUATOR"|"UNKNOWN")
  status_code ("OK"|"ERROR"|"UNSET"), status_message
  context.span_id, context.trace_id, parent_id
  start_time, end_time
  attributes
    input.value, output.value          — raw input/output
    llm.model_name, llm.provider
    llm.token_count.prompt/completion/total
    llm.input_messages.{N}.message.role/content
    llm.output_messages.{N}.message.role/content
    llm.invocation_parameters          — JSON string (temperature, etc.)
    exception.message                  — set if span errored
  annotations[] (with --include-annotations, excludes note)
    name, result { score, label, explanation }
  notes[] (with --include-notes)
    name="note", result { explanation }

Sessions

px session list --limit 10 --format raw --no-progress | jq .
px session list --order asc --format raw --no-progress | jq '.[].session_id'
px session list --include-annotations --include-notes --format raw --no-progress | jq '.[].notes'
px session get <session-id> --format raw | jq .
px session get <session-id> --include-annotations --format raw | jq '.session.annotations'
px session get <session-id> --include-notes --format raw | jq '.session.notes'
px session annotate <session-id> --name reviewer --label pass
px session annotate <session-id> --name reviewer --score 0.9 --format raw --no-progress
px session annotate <session-id> --name reviewer --label pass --identifier "<coding-annotation-id>"  # tag with a coding annotation identifier
px session add-note <session-id> --text "verified by agent"
px session add-note <session-id> --text "verified by agent" --identifier "<coding-annotation-id>"  # tag + upsert on identifier
px session-annotations delete --identifier "<coding-annotation-id>" --all -y              # nuke every annotation tied to this coding annotation identifier
Session JSON shape
SessionData
  id, session_id, project_id
  start_time, end_time
  token_count_prompt, token_count_completion, token_count_total  — cumulative across all LLM spans in the session (int, default 0)
  annotations[] (with --include-annotations, excludes note)
    name, result { score, label, explanation }
  notes[] (with --include-notes)
    name="note", result { explanation }
  traces[]
    id, trace_id, start_time, end_time

Datasets / Experiments / Prompts

px dataset list --format raw --no-progress | jq '.[].name'
px dataset get <name> --format raw | jq '.examples[] | {input, output: .expected_output}'
px dataset get <name> --split train --format raw | jq .    # filter by split
px dataset get <name> --version <version-id> --format raw | jq .
px experiment list --dataset <name> --format raw --no-progress | jq '.[] | {id, name, failed_run_count}'
px experiment get <id> --format raw --no-progress | jq '.[] | select(.error != null) | {input, error}'
px prompt list --format raw --no-progress | jq '.[].name'
px prompt get <name> --format text --no-progress   # plain text, ideal for piping to AI

Annotation Configs

px annotation-config list                                           # list all configs (table view)
px annotation-config list --format raw --no-progress | jq '.[].name' # config names as JSON

GraphQL

For ad-hoc queries not covered by the commands above. Output is {"data": {...}}.

px api graphql '{ projectCount datasetCount promptCount evaluatorCount }'
px api graphql '{ projects { edges { node { name traceCount tokenCountTotal } } } }' | jq '.data.projects.edges[].node'
px api graphql '{ datasets { edges { node { name exampleCount experimentCount } } } }' | jq '.data.datasets.edges[].node'
px api graphql '{ evaluators { edges { node { name kind } } } }' | jq '.data.evaluators.edges[].node'

# Introspect any type
px api graphql '{ __type(name: "Project") { fields { name type { name } } } }' | jq '.data.__type.fields[]'

Key root fields: projects, datasets, prompts, evaluators, projectCount, datasetCount, promptCount, evaluatorCount, viewer.

Docs

Download Phoenix documentation markdown for local use by coding agents.

px docs fetch                                # fetch default workflow docs to .px/docs
px docs fetch --workflow tracing             # fetch only tracing docs
px docs fetch --workflow tracing --workflow evaluation
px docs fetch --dry-run                      # preview what would be downloaded
px docs fetch --refresh                      # clear .px/docs and re-download
px docs fetch --output-dir ./my-docs         # custom output directory

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).

Files

references/open-coding.md

Open Coding

Free-form note-writing against sampled traces, spans, or sessions, before any taxonomy exists. After you pick a sample at the right unit (see Choosing the unit of analysis), read each one and write a short, specific observation of what went wrong. These raw notes feed axial coding, where they get grouped into named failure categories — and ultimately into eval targets or fix priorities.

Reach for this whenever the user wants to look at LLM traffic without a fixed taxonomy yet — e.g., "what's going wrong with this agent", "I just instrumented my app, where do I start", "review these traces", "the chatbot keeps losing context", "what kinds of mistakes is the model making", "help me make sense of these conversations", or any framing that needs grounded observations before categories.

Choosing the unit of analysis

The right unit — trace, span, or session — depends on the question and the system. Pick deliberately before recording; the choice determines whether you call px trace, px span, or px session throughout, and a wrong default is expensive to undo mid-run.

The unit is about where the failure modes you're investigating actually live:

  • Trace — one input → one call graph → one output. Right for classifiers, single-shot summarizers, stateless tool-using agents, single-query RAG. Failure modes that live here: wrong answer, malformed output, missed retrieval, bad tool selection within one request.

  • Span — one operation inside a trace. Right for in-isolation mechanical failures (an exception fired, a tool returned an error response, an output is malformed) or when you can attribute on sight to a specific component. Reach for span when the trace as a whole is fine but one piece inside it is the unit of interest.

  • Session — a sequence of traces sharing a session.id. Right for multi-turn conversational agents, agents with episodic memory, anything where the failure mode is a trajectory: context loss across turns, drift from the user's stated goal, the agent forgetting a stated preference, repeated user clarifications. These failures don't exist on any single trace; they only exist across traces.

Diagnostic — three signals to read
  1. User framing. Tilts session: "conversation", "agent forgot", "drift", "memory", "across turns", "user had to repeat themselves". Tilts trace: "this trace", "this call", "the response was wrong", "wrong output". Tilts span: "exception", "error response", "malformed", "the retrieval failed".

  2. Data shape. Probe before the loop. The session id lives at rootSpan.attributes["session.id"] (it is not a top-level field on the trace JSON), and is "" for traces that aren't session-wired — filter both:

    px trace list --limit 200 --format raw --no-progress \
      | jq '
        [ .[] | .rootSpan.attributes["session.id"] // empty | select(. != "") ]
        | { with_session: length,
            distinct_sessions: (group_by(.) | length),
            median_traces_per_session:
              (group_by(.) | map(length) | sort | .[length/2|floor] // 0) }
      '

    with_session: 0 → sessions not wired; trace is the grain. median_traces_per_session: 1 → single-trace sessions; still trace. median_traces_per_session: 5+ → sessions are meaningful; session is plausibly right.

  3. System type. Open one recent trace and inspect the root span's input. A single user message → one turn or one shot. A message array ([{role: user}, {role: assistant}, ...]) → that's a turn within a longer dialogue; the dialogue lives at the session level.

    px trace get <trace-id> --format raw \
      | jq '.rootSpan.attributes["input.value"] | (try fromjson catch .) | (type, length?)'
Commit out loud, then proceed

State the unit explicitly before recording any note:

"Question: 'the chatbot keeps losing context'. Data: median 7 traces per session, message-array inputs. Recording at the session level; will drop to trace for single-turn observations, span for mechanical failures."

The unit can shift if data demands it — a trace-level investigation that surfaces "the agent never remembers earlier turns" should pivot to session. Record the observation, then refocus the next batch. The unit is a starting hypothesis, not a contract.

Coding annotation identifier (pick this first)

Every artifact this workflow produces — open-coding notes, axial-coding labels, the local sidecar files, and the UI-filter annotation — is tagged with one coding annotation identifier so the run is queryable, revertible, and viewable as a unit. Pick a descriptive, unique identifier before recording any notes. Format suggestion:

coding-run:<short-topic>-<YYYY-MM-DD>

Examples: coding-run:chatbot-context-loss-2026-05-06, coding-run:agent-tool-misuse-q2. Descriptive ids carry meaning for whoever opens the data later — better than an opaque uuid. The coding-run: prefix is a visual convention; the value is the workflow's coding annotation identifier, not a px session id.

Workflow term vs. server annotation name. The skill calls this value the coding annotation identifier. The server-side annotation NAME used for the UI filter is unchanged — coding_session_id — for data compatibility with rows already written. Don't try to rename it.

Pass the identifier explicitly on every px call. A shell variable for readability is fine, but do not rely on shell inheritance — many agent harnesses spawn each command in a fresh subshell, so CODING_ANNOTATION_IDENTIFIER may not propagate.

CODING_ANNOTATION_IDENTIFIER="coding-run:chatbot-context-loss-2026-05-06"

The local sidecar lives at .px/coding/<sanitized-identifier>.jsonl (CWD-relative, matching the .px/docs precedent). Sanitization rule: replace any character not matching [a-zA-Z0-9_-] with - before using the value in the filename — colons, slashes, and other shell-fragile characters get normalized. For CODING_ANNOTATION_IDENTIFIER="coding-run:chatbot-context-loss-2026-05-06" the sidecar path is .px/coding/coding-run-chatbot-context-loss-2026-05-06.jsonl.

Verify this run hasn't already started — uniqueness is a local file check, not a server query:

SLUG=$(echo -n "$CODING_ANNOTATION_IDENTIFIER" | sed 's/[^a-zA-Z0-9_-]/-/g')
SIDECAR=".px/coding/${SLUG}.jsonl"
test ! -f "$SIDECAR" || { echo "Sidecar already exists at $SIDECAR — pick a new identifier or delete the file"; exit 1; }
mkdir -p .px/coding

If $SIDECAR already exists, append a disambiguator (-v2, -dustin, etc.) to CODING_ANNOTATION_IDENTIFIER, re-derive SLUG, and re-check. The agent harness can run open coding and axial coding in independent invocations: each step re-derives SLUG from CODING_ANNOTATION_IDENTIFIER and reads/writes the same file.

Process

  1. Pick a coding annotation identifier — choose a descriptive value and verify the sidecar file does not yet exist (see Coding annotation identifier)

  2. Pick the unit — work through Choosing the unit of analysis and commit to trace, span, or session

  3. Inspect — fetch one entity at the chosen unit (trace / span / session)

  4. Read — input, output, exceptions, tool calls, retrieved context, and (at session level) the trajectory across child traces

  5. Note — write one specific sentence describing what went wrong (or skip if correct)

  6. Record — px {trace,span,session} add-note <id> --text "..." --identifier "$CODING_ANNOTATION_IDENTIFIER" --format raw --no-progress, add/update one JSONL sidecar row for the note, then write the matching UI-filter annotation

  7. Iterate — move to the next entity; repeat until the sample is exhausted or saturation hits

  8. Hand off — axial coding reads the sidecar directly (no shared shell required); see Wrapping up for the UI link

Inspection

Use px to read context at the unit committed in Choosing the unit:

  • Trace unit — read one trace's input → tool calls → retrieved context → output as one story.

  • Span unit — read one operation's input/output and surrounding spans for context.

  • Session unit — read the sequence of traces in order; the trajectory (turns, retrievals, tool-call patterns across traces) is the data, not any single trace's inputs and outputs.

Don't filter the sample by --status-code ERROR. OTel's status_code only flips to ERROR when an instrumentor catches a raised Python exception (network failure, 5xx, parse error). Hallucinations, wrong tone, retrieval misses, and bad tool selection all complete cleanly and arrive as OK or UNSET. Sampling for open coding by --status-code ERROR excludes the population this workflow exists to surface.

# Sample recent traces — the unit of inspection in open coding
px trace list --limit 100 --format raw --no-progress | jq '
  .[] | {trace_id: .traceId, root: .rootSpan.name, status,
         input: .rootSpan.attributes["input.value"],
         output: .rootSpan.attributes["output.value"]}
'

# Trace-level context — all spans in one trace, ordered by start_time
px trace get <trace-id> --format raw | jq '
  .spans | sort_by(.start_time) | map({span_id: .context.span_id, name, status_code,
    input: .attributes["input.value"],
    output: .attributes["output.value"]})
'

# Drill to one span (px span get does not exist; filter via span list)
px span list --trace-id <trace-id> --format raw --no-progress \
  | jq '.[] | select(.context.span_id == "<span-id>")'

# Check existing notes on traces (default) or spans you are about to review
# Notes are stored as annotations with name="note"; use --include-notes (not --include-annotations)
px trace list --include-notes --limit 10 --format raw --no-progress | jq '
  .[] | select((.notes // []) | length > 0)
  | {trace_id: .traceId, notes: [.notes[] | .result.explanation]}
'
# Same shape on spans — swap px trace for px span and use .context.span_id

Always pipe through jq with --format raw --no-progress when scripting.

Recording Notes

Use the add-note command matching the unit committed in Choosing the unit: px trace add-note, px span add-note, or px session add-note. Every call carries an explicit --identifier "$CODING_ANNOTATION_IDENTIFIER" and --format raw --no-progress.

Passing --identifier "$CODING_ANNOTATION_IDENTIFIER" does two things:

  • Tags the note row with the coding annotation identifier on the server, so the cleanup px <entity>-annotations delete --identifier "$CODING_ANNOTATION_IDENTIFIER" --all sweep removes every artifact this run produced.

  • Makes the call upsert on (entity_id, name='note', identifier) — re-running open coding on the same entity within the same coding annotation identifier overwrites the prior note instead of appending a second row. (Without --identifier, the server stamps a unique px-{kind}-note:<uuid> and each call appends.)

After every successful add-note, record one JSONL line in $SIDECAR. The sidecar is what axial coding reads — no server round-trip. It is a content handoff, not code: keep it readable, inspect it directly, and use whatever simple tooling is convenient.

Sidecar JSONL line shape (one per add-note):

{"entity_kind":"trace","entity_id":"<trace-id>","note":"<text>","identifier":"<original identifier value, unsanitized>","ts":"<ISO-8601 UTC>"}

Fields:

  • entity_kind — "trace", "span", or "session" (matches the add-note subcommand used)

  • entity_id — the entity argument passed to add-note (trace id, span id, or session id)

  • note — the --text value, verbatim

  • identifier — the original $CODING_ANNOTATION_IDENTIFIER value, unsanitized; the sanitized form lives only in the filename

  • ts — ISO-8601 UTC timestamp (e.g. 2026-05-08T17:14:09Z) of the local append

If you revise a note for the same entity under the same coding annotation identifier, either replace that row or append a newer row. When duplicate (entity_kind, entity_id) rows exist, the newest ts is the current note. This matches the server upsert behavior of add-note --identifier.

Minimal trace example:

px trace add-note <trace-id> \
  --text "Asked about returns; final answer covered shipping policy instead" \
  --identifier "$CODING_ANNOTATION_IDENTIFIER" \
  --format raw --no-progress

Then add a matching JSONL row to $SIDECAR using the line shape above. For span or session notes, change entity_kind, entity_id, and the px subcommand accordingly.

Bulk auto-tagging by status code (e.g. px span list --status-code ERROR | xargs ... add-note "error") is not open coding — open coding is manual, observation-grounded, and ranges over all failure modes, not just spans where Python raised. Skip the bulk-by-status-code shortcut; it produces fewer, less informative notes than walking traces.

UI-filter annotation

Every entity that receives an open-coding note (or an axial-coding label later) also needs a UI-filter annotation so the Phoenix UI can filter by coding annotation identifier. Phoenix's UI filter language is name-based, not identifier-based — there is no UI primitive for filtering by identifier, so an annotation whose name is the constant coding_session_id and whose label is the coding annotation identifier value is what the wrap-up UI link actually filters on.

The annotation NAME coding_session_id is the load-bearing data key on the server and is unchanged in this rewrite. The skill's workflow term is "coding annotation identifier"; the server key stays coding_session_id for compatibility with rows already written.

Run this once per touched entity, alongside the add-note (and again later when axial coding labels a different entity):

px trace annotate <trace-id> \
  --name coding_session_id \
  --label "$CODING_ANNOTATION_IDENTIFIER" \
  --identifier "$CODING_ANNOTATION_IDENTIFIER"
# or px span annotate / px session annotate at matching levels

The annotation's --identifier matches $CODING_ANNOTATION_IDENTIFIER, so the wrap-up DELETE cleans it up in the same call as the notes and the axial-coding labels.

Fallback write paths (one-line asides):

  • POST /v1/trace_notes and POST /v1/span_notes and POST /v1/session_notes — accept one {data: {trace_id|span_id|session_id, note, identifier}} per request; the optional identifier field upserts on (entity_id, name='note', identifier) when non-empty.

  • @arizeai/phoenix-client addTraceNote, addSpanNote, and addSessionNote wrap the same endpoints and accept an optional identifier field on the note object.

  • The GraphQL endpoint rejects mutations with "Only queries are permitted." — write through px {trace,span,session} add-note or the REST endpoints above.

What Makes a Good Note

Weak noteWhy it's weakGood noteWhy it's strong
"Wrong answer"No observable detail"Said the store closes at 6pm but policy is 9pm"Quotes observed vs. correct value
"Bad tone"Vague judgment"Used first-name greeting for an enterprise support ticket"Specifies the context mismatch
"Hallucination"Labels before observing"Cited a product feature ('auto-renew') that does not exist in the schema"Describes what was fabricated
"Retrieval issue"Category, not observation"Retrieved docs about shipping when the question was about returns"States what was retrieved vs. needed
"Model confused"Opaque"Answered in Spanish when the user wrote in English"Observable and reproducible

Write what you saw, not the category you think it belongs to — categorization happens in axial coding. Short prefixes like TONE: or FACTUAL: are a personal shorthand, not a repo convention.

Saturation

Stop writing notes when observations stop being new. Signals:

  • Repeats — the last 10–15 traces produced notes that describe failures you've already seen.

  • Paraphrase convergence — you catch yourself writing minor variations of earlier notes.

  • Skips outnumber notes — most recent traces are correct and need no note.

At saturation, move on to axial coding to group what you have. Continuing past saturation adds traces but not insight. You do not need to annotate every trace — annotating correct ones dilutes signal.

Listing what this run produced

The local sidecar is the handoff record for notes written this run. Inspect it directly. Each line is one note record; if the same entity appears more than once, use the newest ts as the current note. Missing-file behavior: an absent sidecar means open coding has not yet started for this coding annotation identifier; treat that as zero notes, not an error. Malformed lines are line-local: fix or drop the bad line without editing neighbors.

Wrapping up

When the run is done, share the Phoenix UI link with the user. The link filters the project's traces page by the coding_session_id annotation written alongside each note. The UI route /projects/:projectId expects an encoded GraphQL node ID, not a project name — resolve it via px project get:

project_id=$(px project get "$PHOENIX_PROJECT" --format raw --no-progress | jq -r '.id')
encoded=$(python3 -c 'import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))' \
  "annotations['coding_session_id'].label == '$CODING_ANNOTATION_IDENTIFIER'")
echo "Phoenix UI: $PHOENIX_HOST/projects/$project_id/traces?filterCondition=$encoded"

If the user wants to discard everything this run produced, three identifier-bound deletes handle the server side and one rm handles the local sidecars. Confirm with the user before running — this is destructive. Each call requires --all (or both --start-time and --end-time) to authorize the sweep; --identifier filters further but never authorizes on its own. Set PHOENIX_CLI_DANGEROUSLY_ENABLE_DELETES=true first if not already exported:

for kind in trace span session; do
  px "$kind-annotations" delete \
    --identifier "$CODING_ANNOTATION_IDENTIFIER" \
    --all -y \
    --format raw --no-progress
done
rm -f "$SIDECAR" ".px/coding/${SLUG}-axial.jsonl"

Each px <entity>-annotations delete call covers notes, structured annotations, and the coding_session_id annotation in one shot because they share the underlying annotation table.

Principles

  • One coding annotation identifier per run — every server artifact and every sidecar line carries the same $CODING_ANNOTATION_IDENTIFIER; never mint a per-stage id.

  • Pass --identifier explicitly — every px call gets --identifier "$CODING_ANNOTATION_IDENTIFIER"; do not rely on inherited env vars across harness-spawned subshells.

  • Sidecar is the handoff record for notes — axial coding reads from the local sidecar, not from the server; if an entity appears more than once, the newest ts wins.

  • Free-form over structured — do not pre-commit to a taxonomy during open coding; categories emerge in axial coding.

  • Specific over general — quote or paraphrase the observed failure; vague labels ("bad response") carry no signal.

  • Context before labeling — inspect input, output, and retrieved context before writing any note.

  • Iterate before categorizing — work through the full sample first; resist grouping while still collecting.

  • Skip is valid — a correct span needs no note; annotating everything dilutes signal.

  • Revert is opt-in — the wrap-up DELETE only runs after explicit user confirmation; the default path prints the UI link and stops.

Requirements and compatibility

Written for: GitHub Copilot

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an API key, Needs Node.js

The author says: Requires Node.js (for npx) or global install of @arizeai/phoenix-cli. Optionally requires jq for JSON processing.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
arize-ai
Publisher
GitHub (Official)
Distributed by
GitHub
Source
github/awesome-copilot · skills/phoenix-cli · commit 797337f
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (written by AutoGineer)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by arize-ai or GitHub.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download phoenix-cli.zip