Session filters require Phoenix 19.18.0+.
Where filters work
Support for session filters in the REST API and Python client is tracked in
#15099 and
#15112.
The Experiment Compare view has its own filter language for filtering experiment runs,
separate from the span, trace, and session filter languages.
Span filters
A span filter matches individual spans by built-in fields likespan_kind, latency_ms, and
input.value:
metadata[...] is shorthand for the metadata attribute:
is None matches spans that an
annotation hasn’t been written to yet:
parent_span is None to match root spans, including spans whose recorded parent was never
received. To match only spans with no parent id at all, use parent_id is None:
- Trace-level annotations:
trace_annotations["name"]filters spans by annotations on their parent trace, with the same.score/.label/.explanation/ existence syntax asannotations. - Enum values: string literals compared against
span_kindorstatus_codeare uppercased automatically —span_kind == 'llm'andspan_kind == 'LLM'match the same spans. - Unknown names: a name that isn’t a built-in field is read as an attribute path, so a typo filters on a nonexistent attribute and matches nothing rather than producing an error.
SpanQuery().where(...)
export path.
Trace filters
A trace filter matches whole traces by fields likelatency_ms and by values rolled up from their
spans. Rollups with no matching data are 0, never null:
token_count_prompt, token_count_completion, and token_count_total. Cost
rollups include prompt_cost, completion_cost, and total_cost. Use tool_span_count and
llm_span_count to count spans by kind.
Read input, output, attributes, user.id, and metadata[...] from the trace’s root span:
trace_annotations["name"]. It exposes .score,
.label, and .explanation; a lookup without one of these fields checks whether the annotation
exists:
trace_annotations[...] for
trace annotations or the span_annotations collection for span annotations.
Comprehensions quantify and aggregate over the trace’s spans, trace annotations, span
annotations, and span cost details:
children and parent_span, so you can filter by parent-child relationships:
- Strict names: unknown names are rejected with a “did you mean” suggestion, unlike span filters, which fall back to attribute paths.
- Loop variables only: inside a comprehension, reference the loop variable’s fields
(
span.latency_ms), not bare trace-level names. - The available aggregate, collection, and element-field names are project-specific — see Finding field names.
Session filters
A session groups the traces of one conversation. A session filter can test aggregate properties of the whole session and inspect the traces and spans inside it. Aggregates roll up the session’s traces and spans. An aggregate with no matching data is0, never
null; dividing by an aggregate that is 0 matches nothing rather than producing an error:
first_input
and last_output are the session’s opening input and final output as strings — use ==, in, or
is None. any_input and any_output test containment across all of the session’s inputs and
outputs, and support only in / not in:
any and all ask a yes/no question; len, sum, max, and min reduce to a number:
traces element exposes its own spans, so you can ask per-turn questions with one level of
nesting:
- Strict names: unknown names are rejected with a “did you mean” suggestion, unlike span filters, which fall back to attribute paths.
- Loop variables only: inside a comprehension, reference the loop variable’s fields
(
span.latency_ms), not bare session-level names. - The available aggregate, collection, and element-field names are project-specific — see Finding field names.
Finding field names
Field names are project-specific: attribute keys and annotation names come from your data. To discover what’s available:- In any filter bar, start typing to get a typeahead of the names available in your project, grouped by kind (fields, aggregates, collections, attributes, annotations).
- For traces and sessions, query the
traceFilterVocabularyandsessionFilterVocabularyGraphQL fields to enumerate valid names programmatically. Span filters have no equivalent endpoint; use the filter-bar typeahead.
Syntax rules
These rules apply to span, trace, and session filters unless a note says otherwise.- Operators. Compare with
==!=<<=>>=(chained comparisons like0.5 < latency_ms < 1000are supported); combine conditions withand/or/not; test membership within/not in; check for missing values withis None/is not None; and do arithmetic on numeric fields with+-*/%(e.g.num_traces_with_error / num_traces,total_cost - prompt_cost). The whole expression must be a condition, not a bare value. - Annotations. Use
trace_annotations["name"]in a trace filter andsession_annotations["name"]in a session filter. Span filters useannotations["name"]and its legacy aliasevals["name"]; they also accepttrace_annotations["name"]for annotations on the containing trace. Each lookup exposes.score,.label, and.explanation, and a lookup without one of these fields is an existence check. in/not inignore case. Containment against text is case-insensitive:'refund' in first_inputmatchesREFUND please. Equality (==/!=) and membership in a literal list (span_kind in ['LLM']) are exact.- Missing values match nothing. When a value is absent, every comparison against it is false —
including
!=. Useis None/is not Noneto match missing values (see the note below). - Datetime literals need a timezone offset. Write
start_time > '2026-07-01T00:00:00+00:00'or use a trailingZ; a literal without an offset is rejected as ambiguous. - Function calls.
float()andstr()convert an attribute of unknown type; span filters also acceptint(), which behaves likefloat()and does not truncate. Trace and session comprehensions accept the reducersany/all/len/max/min/sum. All other function and method calls (e.g.name.startswith(...),len(span_id)) are rejected, as are**,//, and the bitwise operators&|^. Strings are not implicitly converted to numbers.
Missing values behave differently than in Python. In Python, To also match rows where the value is missing, spell it out:
None != 'premium' evaluates to
True. In a filter, a span with no user.tier attribute matches neither of these expressions:attributes['user.tier'] != 'premium' or attributes['user.tier'] is None.
