Tag and filter traces
What you'll do: attach tags and metadata to traces, filter your trace list by tag or metadata, and discover which tags and metadata keys your team is already using.
Tags are short labels (prod, nl, staging) that help you group and filter
traces. Metadata is a freeform JSON object ({"env": "prod", "lang": "nl", "userId": "abc123"}) that lets you attach structured context without changing
your trace schema. Both are set on creation and merge when you append to an
existing trace — tags are union-merged (deduplicated), metadata is
shallow-merged (new keys win).
A standalone script exercising this whole flow lives at scripts/guides/tag-and-filter-traces on GitHub — Python and TypeScript versions.
1. Set tags and metadata when creating a trace
The fastest way to attach tags is through the gateway headers on every completion call. This works from any HTTP client — no SDK changes needed.
- curl
- Python
- Node.js
Pass x-trace-tags (comma-separated) and x-trace-metadata (JSON string) as
request headers:
curl -X POST "$ACRUXCORE_BASE_URL/chat/completions" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-H "x-trace-tags: prod, nl" \
-H 'x-trace-metadata: {"env": "prod", "lang": "nl"}' \
-d '{
"model": "support-model",
"messages": [{"role": "user", "content": "Hello"}]
}'
The trace created by this call will have tags: ["prod", "nl"] and
metadata: {"env": "prod", "lang": "nl"}.
The SDK forwards tags and metadata as gateway headers automatically.
Pass them in the trace options on any chat() or run_tool_loop() call:
from acruxcore import AcruxCore
client = AcruxCore()
result = await client.chat(
"support-model",
[{"role": "user", "content": "Hello"}],
trace={
"tags": ["prod", "nl"],
"metadata": {"env": "prod", "lang": "nl"},
},
)
The SDK forwards tags and metadata as gateway headers automatically.
Pass them in the trace options on any chat() or runToolLoop() call:
import { AcruxCore } from "@acruxcoreai/sdk";
const client = new AcruxCore();
const result = await client.chat({
model: "support-model",
messages: [{ role: "user", content: "Hello" }],
trace: {
tags: ["prod", "nl"],
metadata: { env: "prod", lang: "nl" },
},
});
2. Filter traces by tag
Once traces have tags, you can filter them. Tags use AND matching — a
query for prod and nl returns only traces that carry both.
- curl
- Python
- Node.js
Repeat the tags parameter for AND matching:
curl -g -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces?tags=prod&tags=nl"
{
"data": [
{
"id": "1b2d4e99-8bfb-49f0-af63-97ae6da301f0",
"name": "support-query",
"status": "ok",
"tags": ["prod", "nl"],
"spanCount": 1,
"startedAt": "2026-07-05T12:25:11.000Z"
}
],
"total": 1,
"page": 1,
"limit": 20
}
curl treats [ and ] in URLs as globbing syntax. Always pass -g to
disable that, or URL-encode the brackets (%5B/%5D).
The SDK's list_traces() method does not yet support tag or metadata
filtering. Use the REST API directly:
import httpx
response = httpx.get(
f"{base_url}/traces",
headers={"Authorization": f"Bearer {api_key}"},
params={"tags": ["prod", "nl"]},
)
traces = response.json()
The SDK's listTraces() method does not yet support tag or metadata
filtering. Use the REST API directly:
const params = new URLSearchParams();
params.append("tags", "prod");
params.append("tags", "nl");
const response = await fetch(`${baseUrl}/traces?${params}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const traces = await response.json();
3. Filter traces by metadata
Metadata filtering uses bracket notation and also AND matching — every supplied key/value pair must be present in the trace's metadata.
- curl
- Python
- Node.js
curl -g -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces?metadata[env]=prod&metadata[lang]=nl"
{
"data": [
{
"id": "1b2d4e99-8bfb-49f0-af63-97ae6da301f0",
"name": "support-query",
"status": "ok",
"tags": ["prod", "nl"],
"spanCount": 1,
"startedAt": "2026-07-05T12:25:11.000Z"
}
],
"total": 1,
"page": 1,
"limit": 20
}
response = httpx.get(
f"{base_url}/traces",
headers={"Authorization": f"Bearer {api_key}"},
params={"metadata[env]": "prod", "metadata[lang]": "nl"},
)
traces = response.json()
const params = new URLSearchParams();
params.append("metadata[env]", "prod");
params.append("metadata[lang]", "nl");
const response = await fetch(`${baseUrl}/traces?${params}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const traces = await response.json();
4. Discover which tags and metadata keys exist
You don't need to remember every tag by hand. The facets endpoint returns all distinct tags and metadata keys your team has used, and a second endpoint returns all distinct values for a single metadata key.
- curl
- Node (SDK)
- Python (SDK)
- Node (fetch)
- Python (httpx)
Get all tags and metadata keys:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces/facets"
{
"tags": ["prod", "staging", "nl", "de", "support", "billing"],
"metadataKeys": ["env", "lang", "userId", "sessionId"]
}
Get all values for one metadata key:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces/facets/values?key=env"
{
"values": ["prod", "staging", "dev"]
}
The response carries no key field — only values. A caller that
expects the request's key echoed back in the body won't find it there.
The SDK's hub.traces namespace wraps both routes. Real output below, from a
team tagged prod/nl with metadata {"env": "prod", "lang": "nl"} on one
trace (plus an unrelated trace-analytics-check tag from other testing on the
same team — listFacets() returns everything the team has ever used, not a
scoped subset):
import AcruxCore from '@acruxcoreai/sdk';
const hub = new AcruxCore();
const facets = await hub.traces.listFacets();
console.log(facets);
// { tags: [ 'nl', 'prod', 'trace-analytics-check' ], metadataKeys: [ 'checkRunId', 'env', 'lang' ] }
const values = await hub.traces.getFacetValues('env');
console.log(values);
// { values: [ 'prod' ] }
from acruxcore import AcruxCore
hub = AcruxCore()
facets = await hub.traces.list_facets()
print(facets)
# TraceFacets(tags=['nl', 'prod', 'trace-analytics-check'], metadata_keys=['checkRunId', 'env', 'lang'])
values = await hub.traces.get_facet_values("env")
print(values)
# FacetValuesResult(values=['prod'])
// List all tags and metadata keys
const facets = await fetch(`${baseUrl}/traces/facets`, {
headers: { Authorization: `Bearer ${apiKey}` },
}).then(r => r.json());
// List all values for a metadata key
const values = await fetch(
`${baseUrl}/traces/facets/values?key=env`,
{ headers: { Authorization: `Bearer ${apiKey}` } }
).then(r => r.json());
# List all tags and metadata keys
facets = httpx.get(
f"{base_url}/traces/facets",
headers={"Authorization": f"Bearer {api_key}"},
).json()
# List all values for a metadata key
values = httpx.get(
f"{base_url}/traces/facets/values",
headers={"Authorization": f"Bearer {api_key}"},
params={"key": "env"},
).json()
5. Search what was actually said
Tags and metadata only find a trace if someone thought to label it. Often the only thing you remember is the conversation itself — a customer said they wanted to visit London, and you need that run.
q searches the captured request and response as well as trace and span names
and attributes:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces?q=london"
When a word appears on both sides of the exchange, q_in picks one:
q_in=input— only what was sent to the model.q_in=output— only what the model answered.q_in=name— only trace and span names, no payload text.
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces?q=london&q_in=input"
Only traces whose payloads were captured can be searched by content. If a search returns nothing you expected, check payload capture is on — see Configure trace payload capture.
6. Filter by prompt
prompt_version_id narrows to one exact version. prompt_id matches every
version of a prompt, which is what you want when several versions have shipped
and you are asking how the prompt as a whole behaves:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces?prompt_id=$PROMPT_ID"
A trace matches when at least one of its spans used the prompt. One run can use
several prompts, so the same trace can appear under more than one prompt_id.
7. Filter the feedback feed with the same words
Everything above also filters GET /traces/feedback, the team-wide feed of
ratings and comments — the list you work through when deciding which production
traffic becomes evaluation data:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces/feedback?prompt_id=$PROMPT_ID&rating=down&has_comment=true"
Four filters there describe the feedback row rather than its trace:
rating—up,down, ornone.source—user,developer,end_user,api.label— exact match.has_comment—truefor rows carrying a written critique.
The same criteria object builds a dataset in one call — see Improve a prompt from feedback.
8. Filter in the dashboard, and save a view
Everything above is one input box in the dashboard. Type into the filter bar on Traces or Feedback and it suggests the filter you are reaching for:
tag:andmeta.<key>:suggest values your team has actually used.prompt:searches your prompts by name, so you never need a version id.input:,output:andname:narrow a text search to one part of the trace.status:,rating:,source:,comment:anderror_type:suggest their allowed values.error_code:suggests the failure slugs your own tools have declared — see Report a tool failure from your own code.score>,latency>,cost>andtokens>take a number.- Anything with no prefix becomes a plain search across names, input and output.
Each committed filter becomes a chip. Backspace on an empty box removes the last one, and Clear all filters removes them all. The filters live in the URL, so a filtered list can be linked and shared, and back/forward work.
You can type several filters in one go — rating:down comment:yes commits both
chips at once. A value with a space in it still counts as one filter, because
tag alone names nothing: tag:my tag is a single tag called "my tag". If a
value is one the filter does not accept, nothing is applied, the text stays in
the box, and the bar says what that filter takes.
Save the ones you use often. Press Views → Save current filters, give it a name, and it is one click for everyone on the team from then on. A view stores the filter string exactly as it was, and any member can rename or delete it.
The same bar appears inside Add example → From feedback on a dataset. Filter there, and Add all N matching pulls in every row the criteria select — including rows on pages you never opened.
9. Group traces under a session
Tags and metadata live on individual traces. When you want several traces to roll up as one conversation — a support ticket, a multi-turn chat, a single agent run — give them a shared session id. All traces carrying the same session id appear together under Observability → Sessions in the dashboard.
A session id is just a string you choose (support-ticket-421, agent-run-abc).
It has no tags or metadata of its own — filter your traces by tag first, then
the matching traces group under whatever sessions they belong to.
- curl
- Python
- Node.js
Pass x-session-id as a request header on any gateway call:
curl -X POST "$ACRUXCORE_BASE_URL/chat/completions" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-H "x-session-id: support-ticket-421" \
-H "x-trace-tags: prod, support" \
-d '{
"model": "support-model",
"messages": [{"role": "user", "content": "Hello"}]
}'
Send the same x-session-id on every call that belongs to the conversation;
each trace keeps its own tags and metadata but groups under the one session.
Pass session_id in the trace options alongside your tags and metadata:
result = await client.chat(
"support-model",
[{"role": "user", "content": "Hello"}],
trace={
"session_id": "support-ticket-421",
"tags": ["prod", "support"],
},
)
The same session_id works on run_tool_loop() — every call inside the
loop inherits the session.
Pass sessionId in the trace options alongside your tags and metadata:
const result = await client.chat({
model: "support-model",
messages: [{ role: "user", content: "Hello" }],
trace: {
sessionId: "support-ticket-421",
tags: ["prod", "support"],
},
});
The same sessionId works on runToolLoop() — every call inside the
loop inherits the session.
Read it back from a trace with hub.traces.get():
detail = await hub.traces.get(trace_id)
print(detail.trace.session_id) # "support-ticket-421"
const detail = await hub.traces.get(traceId);
console.log(detail.trace.sessionId); // "support-ticket-421"
For deeper coverage of sessions — listing them, filtering traces by session, and reading aggregate stats — see the Using sessions and traces guide.
Merge behavior
When you append to an existing trace (sending a second request with the same trace id), tags and metadata merge rather than overwrite:
- Tags are union-merged — if the trace already has
["prod"]and you send["nl"], the result is["prod", "nl"]. Duplicates are removed. - Metadata is shallow-merged — existing keys stay, new keys are added, and overlapping keys take the new value.
This means you can set base tags once (e.g. prod) and add per-request tags
(e.g. support, billing) without losing either.
Summary
| What | How |
|---|---|
| Set tags on a trace | x-trace-tags: a, b header (gateway) or tags: [...] in trace input (SDK) |
| Set metadata on a trace | x-trace-metadata: {"k":"v"} header (gateway) or metadata: {...} in trace input (SDK) |
| Filter by tags | ?tags=a&tags=b (AND match) |
| Filter by metadata | ?metadata[k1]=v1&metadata[k2]=v2 (AND match) |
| Search the conversation | ?q=london, narrowed with ?q_in=input|output|name |
| Filter by prompt | ?prompt_id=<id> (any version) or ?prompt_version_id=<id> (one version) |
| Filter by how a run failed | ?error_type=tool_declared, ?error_code=<your slug>, ?has_warning=true |
| Filter the feedback feed | Same params on GET /traces/feedback, plus rating, source, label, has_comment |
| Save a filter set | Views → Save current filters, or POST /trace-views |
| Discover tags | GET /traces/facets |
| Discover declared error codes | GET /traces/facets (errorCodes) |
| Discover metadata values | GET /traces/facets/values?key=<key> |
| Group traces under a session | x-session-id: <id> header (gateway) or sessionId / session_id in trace input (SDK) |
The Python and Node.js SDKs forward tags and metadata to the gateway on
chat() and runToolLoop() calls, but list_traces() / listTraces()
do not yet accept tag or metadata filter parameters. Use the REST API
directly for filtering until SDK support is added.