AIO Span and Trace Masking

Read-time masking lets you hide or transform parts of traced data (spans and traces) as they are returned by the AIO read APIs and UI, without ever touching the stored data. You configure it per workspace, and optionally narrow it to a single project. The problem it solves: sensitive content (system prompts, tool calls, PII in message payloads) is often captured in traces but should not be visible to everyone who reads them — and you need to control that visibility without re-ingesting or deleting the underlying records.

A rule does one of two things, depending on the action you choose:

Action

What it does

Applies to

DROP_AND_REWIRE

Removes matching spans from list/tree responses and reparents their children to the nearest surviving ancestor, so the tree stays connected.

Spans only

REDACT

Blanks JSON fields in place on matching entities — it sets the value a JsonPath selects to null or to a string.

Spans, Traces (and Threads)

Use DROP_AND_REWIRE when you want a whole span to disappear from listings (for example, noisy tool-call spans), and REDACT when you want the entity to remain visible but with specific values blanked.

Important

REDACT blanks in place — it sets the matched values to a constant. It cannot delete keys or remove array elements, so array lengths and positions are always preserved. A matched array element becomes null / "[REDACTED]"; it does not disappear from the array.

1. Authentication & scoping

All endpoints live under /aio/api/v1/private/aio/masking-rules. You authenticate with your workspace access credentials, sent as request headers:

accessKey: <your access key> secretKey: <your secret key>

The workspace a rule belongs to is taken from the tenant subdomain in the URL you call — never from the request body. In https://aio-test.acceldata.app/…, the subdomain aio-test is the workspace, and it becomes the rule's workspaceId. Because scoping is tied to the URL rather than anything you pass in the body, every operation is automatically isolated to the workspace you're calling — you cannot accidentally create or touch a rule in another workspace.

If your credentials are missing or invalid, the request is rejected with 403 Forbidden.

For brevity, the examples below assume you've set:

BASE="https://aio-test.acceldata.app/aio/api/v1/private" AUTH=(-H "accessKey: <your-access-key>" -H "secretKey: <your-secret-key>")

2. Endpoints

There are four endpoints — list, create, toggle, and delete. Paths are relative to $BASE (…/aio/api/v1/private).

Method

Path

Description

Success

Errors

GET

/v1/private/aio/masking-rules

List all rules in your workspace (enabled and disabled).

200 + [MaskingRule]

403

POST

/v1/private/aio/masking-rules

Create a rule.

201 + MaskingRule

400, 403

PATCH

/v1/private/aio/masking-rules/{id}

Enable/disable a rule. Body: {"enabled": <bool>}.

200 + MaskingRule

404, 403

DELETE

/v1/private/aio/masking-rules/{id}

Delete a rule.

204

403

Note There is no full-update (PUT) endpoint. To change a rule's filters or patches, delete it and create a new one.

Your changes take effect immediately: creating, deleting, or toggling a rule invalidates the rule cache. (When nothing changes, the effective-rules cache simply expires on a short TTL of about 60 seconds.)

3. The MaskingRule object

This is the shape of a rule as the API returns it. When you create one, you only supply the fields that describe what to match and what to do — the server fills in the rest.

{ "id": "15a60bd6-31b8-44a7-9740-ddbdaadc7665", // UUID, server-assigned on create (omit/null in POST) "workspaceId": "aio-test", // server-assigned from the tenant subdomain in the URL (omit/null in POST) "projectId": null, // UUID or null. null = workspace-wide; set = one project only "name": "disable-tool-call-spans", // required, human-readable label "enabled": true, // default true "entityKind": "SPAN", // "SPAN" | "TRACE" (uppercase) "action": "DROP_AND_REWIRE", // "DROP_AND_REWIRE" | "REDACT" (uppercase) "filters": [ /* Filter[] — required, non-empty */ ], "patches": [ /* Patch[] — required for REDACT; ignored for DROP_AND_REWIRE */ ], "priority": 0, // int, default 0 (lower applied first) "createdBy": "user@acceldata.io", // server-managed "lastUpdatedBy": "user@acceldata.io" // server-managed }

Here is what each field you can set means:

Field

Description

Default

Required

name

A human-readable label so you can recognize the rule later.

Yes

entityKind

SPAN or TRACE (uppercase). This is enforced: a SPAN rule only ever matches spans, and a TRACE rule only ever matches traces (and threads). They never cross over, so pick the one that matches the data you're targeting.

Yes

action

DROP_AND_REWIRE (spans only) or REDACT.

Yes

filters

The predicates that decide which entities the rule matches. Must be non-empty.

Yes

patches

What to blank, for REDACT rules. Required and non-empty for REDACT; ignored for DROP_AND_REWIRE.

[]

For REDACT

enabled

Whether the rule is active. Set it to false to keep the rule but stop masking.

true

No

projectId

A project UUID to scope the rule to a single project, or null to apply it across the whole workspace.

null

No

priority

An integer that orders rules when several apply; lower values are applied first.

0

No

id, workspaceId

Server-assigned — omit them (or send null) when you POST.

No

Note: entityKind and action are uppercase enum names. createdAt / lastUpdatedAt are stored but not surfaced in responses in v1.

4. Filters (filters[]) — choosing which entities a rule matches

A filter is a single predicate. A rule matches an entity only when every filter matches (logical AND), so adding more filters narrows what the rule applies to. Because a rule must never match everything, an empty filters array is rejected with 400.

{ "field": "type", // what to look at "operator": "=", // how to compare "value": "tool", // the value you're comparing against (a string) "key": null // optional JsonPath; only meaningful for input_json/output_json/metadata }

5. Available filter fields

These are the fields you can filter on today. (The data model defines a broader canonical set mirroring Opik's filters — duration, usage tokens, feedback scores, and so on — but if you filter on a field that isn't in the tables below, it currently evaluates to no match rather than returning an error. So double-check your field names against these tables.)

Spans (entityKind: "SPAN"):

Field

What you match against

id, trace_id

The span/trace UUID

name

The span name

type

The span type — general, tool, llm, guardrail (lowercase)

model, provider

The LLM model / provider

source

The span source (e.g. sdk)

tags

The tag set

input, output

The stringified input/output JSON — handy with contains

input_json, output_json, metadata

The JSON value; add a key (JsonPath) to match a nested value

Traces (entityKind: "TRACE") — the same as spans minus type/model/provider, plus thread_id:

Field

What you match against

id, name, source, tags

As above

thread_id

The thread id

input, output

The stringified input/output

input_json, output_json, metadata

The JSON value (+ optional key)

Operators you can use: =, !=, contains, not_contains, starts_with, ends_with, >, >=, <, <=, is_empty, is_not_empty.

A few things to keep in mind when you pick an operator:

  • = / != are case-sensitive, but contains / starts_with / ends_with are case-insensitive.

  • The numeric operators (>, >=, <, <=) compare numerically; if the value isn't numeric, the filter is treated as no-match.

  • key (a JsonPath like $.env) is only meaningful for input_json / output_json / metadata. Use it to reach inside the JSON, e.g. {"field":"metadata","key":"$.env","operator":"=","value":"prod"} matches only entities whose metadata has env set to prod.

6. Patches (patches[]) — for REDACT only: what to blank

A patch tells a REDACT rule exactly which value to blank and what to replace it with. You need at least one patch on a REDACT rule (a REDACT rule with no patches is rejected 400), and patches are ignored entirely on DROP_AND_REWIRE rules.

{ "field": "INPUT", // which root field to edit: INPUT | OUTPUT | METADATA | ERROR_INFO "jsonPath": "$.messages[?(@.type=='tool')]", // a jayway JsonPath selecting the value(s) to blank "constant": null // the replacement: null, or a string (e.g. "[REDACTED]") — nothing else }

How to fill each part:

  • field selects which root part of the entity you're editing — one of INPUT, OUTPUT, METADATA, ERROR_INFO (uppercase).

  • jsonPath (jayway syntax) selects the value or values to set to constant. You can target:

    • a single property — $.system_prompt

    • every element that matches a predicate — $.messages[?(@.type=='tool')]

    • the whole field — $, which replaces the entire input/output/metadata

  • constant must be null or a string. Numbers, booleans, and objects are rejected. Use null when you want a clean, removal-like blank, or a marker such as "[REDACTED]" when you want readers to see that something was withheld.

Two behaviors to be aware of so you aren't surprised:

  • A jsonPath that matches nothing is a no-op — the field is left untouched. So if a redact "isn't working," check the path first.

  • ERROR_INFO patches always blank the whole errorInfo field; the jsonPath/constant are not applied to sub-paths.

Warning: Patches are fail-closed. If a patch's JsonPath is malformed, the entire target field is blanked rather than returned unredacted — so a broken path errs on the side of hiding more, not less.

7. Where masking is (and isn't) applied

It helps to know exactly which read paths honor your rules, because masking is not applied everywhere.

Masked (read path):

  • Spans: list (find), getById (REDACT only — see below), getByTraceIds, search.

  • Traces: get, list (find), search.

  • Threads: find, getById, search. A TRACE REDACT rule also blanks the thread's firstMessage (treated as input) / lastMessage (treated as output).

Behaviors worth knowing:

  • DROP only affects lists and trees, never a direct single-span GET /spans/{id} (which is REDACT-only). So a deep link to a dropped span still resolves — it just won't show up in listings. When dropping happens in a paged list, the page total/size is adjusted to the post-mask count.

  • Traces are never dropped (REDACT only); DROP_AND_REWIRE is a span-only concept.

  • For spans, REDACT runs before DROP, so a span may be redacted and then dropped.

Not masked (out of scope in v1): stats/aggregations, exports, getByIds (internal flows such as experiment-item creation), and trace→dataset materialization. The data also stays unmasked in the database itself — masking is purely a read-time concern.

8. Examples

8.1 Drop all tool spans (workspace-wide)

Hide every tool-call span from your span listings:

curl --location --globoff 'https://{{tenantUrl}}/aio/api/v1/private/aio/masking-rules' \ --header 'accessKey: {{accessKey}}' \ --header 'secretKey: {{secretKey}}' \ --header 'Content-Type: application/json' \ --data '{ "name": "disable-tool-call-spans", "entityKind": "SPAN", "action": "DROP_AND_REWIRE", "filters": [ { "field": "type", "operator": "=", "value": "tool" } ], "patches": [], "enabled": true }'

Effect: every span with type = tool is removed from span listings, and its children are reparented to the nearest surviving ancestor so the tree stays connected.

8.2 Drop spans named RunnableSequence in a specific project

curl --location --globoff 'https://{{tenantUrl}}/aio/api/v1/private/aio/masking-rules' \ --header 'accessKey: {{accessKey}}' \ --header 'secretKey: {{secretKey}}' \ --header 'Content-Type: application/json' \ --data '{ "name": "drop-runnable-sequence", "projectId": {{projectID}}, "entityKind": "SPAN", "action": "DROP_AND_REWIRE", "filters": [ { "field": "name", "operator": "=", "value": "RunnableSequence" } ], "patches": [], "enabled": true }'

This uses an exact match. If you also want to catch variants like RunnableSequence<...>, switch the operator to "starts_with".

8.3 Redact system/tool messages in a span's input (nested array)

ChatBedrockConverse spans carry input.messages as a nested array (messages[x][y]). This rule blanks the message objects whose type is system or tool, in place:

curl --location --globoff 'https://{{tenantUrl}}/aio/api/v1/private/aio/masking-rules' \ --header 'accessKey: {{accessKey}}' \ --header 'secretKey: {{secretKey}}' \ --header 'Content-Type: application/json' \ --data '{ "name": "redact-bedrock-system-tool-messages", "projectId": {{projectID}}, "entityKind": "SPAN", "action": "REDACT", "filters": [ { "field": "name", "operator": "=", "value": "ChatBedrockConverse" } ], "patches": [ { "field": "INPUT", "jsonPath": "$.messages[*][?(@.type == '\''system'\'' || @.type == '\''tool'\'')]", "constant": null } ], "enabled": true }'

Result: the matching message objects become null, the array keeps its length, and human/ai messages are untouched. If you'd rather show a visible marker than a blank, set "constant": "[REDACTED]".

8.4 Redact tool messages in a trace's output (flat array)

The LangGraph trace (the root in the traces view — not a span) has output.messages as a flat array (messages[y]), so this is a TRACE rule:

curl --location --globoff 'https://{{tenantUrl}}/aio/api/v1/private/aio/masking-rules' \ --header 'accessKey: {{accessKey}}' \ --header 'secretKey: {{secretKey}}' \ --header 'Content-Type: application/json' \ --data '{ "name": "redact-langgraph-tool-messages", "projectId": {{projectID}}, "entityKind": "TRACE", "action": "REDACT", "filters": [ { "field": "name", "operator": "=", "value": "LangGraph" } ], "patches": [ { "field": "OUTPUT", "jsonPath": "$.messages[?(@.type == '\''tool'\'')]", "constant": null } ], "enabled": true }'

Watch the path shape, because it's the most common mistake: a nested array needs $.messages[*][?(...)], while a flat array needs $.messages[?(...)]. The wrong shape simply matches nothing and silently leaves the data unmasked.

8.5 List, toggle, delete

list all rules in your workspace

curl --location --globoff 'https://{{tenantUrl}}/aio/api/v1/private/aio/masking-rules' \ --header 'accessKey: {{accessKey}}' \ --header 'secretKey: {{secretKey}}'

disable a rule (brings the data back) — re-enable later with "enabled": true

curl --location --globoff --request PATCH 'https://{{tenantUrl}}/aio/api/v1/private/aio/masking-rules/{{ruleId}}' \ --header 'accessKey: {{accessKey}}' \ --header 'secretKey: {{secretKey}}' \ --data '{ "enabled": false }'

delete a rule

curl --location --globoff --request DELETE 'https://{{tenantUrl}}/aio/api/v1/private/aio/masking-rules/{{ruleId}}' \ --header 'accessKey: {{accessKey}}' \ --header 'secretKey: {{secretKey}}'

Disabling is the safe, reversible way to stop masking; deleting removes the rule for good.

9. Errors

If a request fails, the status code tells you why:

Status

When

400 Bad Request

Empty filters; a REDACT rule with empty patches; a patch constant that is not null/string; a blank jsonPath.

403 Forbidden

Missing or invalid auth credentials.

404 Not Found

A PATCH/DELETE for an id that doesn't exist in your workspace (you cannot touch another workspace's rules).