Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 62 additions & 11 deletions content/docs/overview/agent-traces/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,39 @@ publishedAt: "2026-05-22"

Use the Agent Traces API when you want the same activity timeline shown in the dashboard, but as JSON for analysis, replay, or internal tooling.

:::callout
type: info
### SDK availability
The Node and Python examples below target the upcoming Steel SDK release that adds `sessions.traces()`. Use the REST endpoint until that SDK version is published.
:::

### SDK

<CodeTabs storage="languageSwitcher">

```typescript !! Typescript -wcn
import Steel from 'steel-sdk';

const client = new Steel();

const trace = await client.sessions.traces(sessionId, {
limit: 500,
});
```

```python !! Python -wcn
from steel import Steel

client = Steel()

trace = client.sessions.traces(
session_id,
limit=500,
)
```

</CodeTabs>

### GET /v1/sessions/\:id/agent-traces

Returns the trace timeline for a finished or in-progress session.
Expand Down Expand Up @@ -44,15 +77,21 @@ const trace = await response.json();
}
],
"total": 1,
"hasMore": false
"hasMore": false,
"nextCursor": null,
"schemaVersion": "1.0",
"truncated": false
}
```

| Field | Type | Notes |
| ----- | ---- | ----- |
| `events` | array | Timeline activities in chronological order. |
| `total` | number | Number of activities returned. |
| `total` | number | Total number of matching activities. |
| `hasMore` | boolean | Whether more activity data is available. |
| `nextCursor` | string or null | Opaque cursor for the next page. |
| `schemaVersion` | string | Version of the normalized activity schema. |
| `truncated` | boolean | Whether the retained source could not be scanned completely. Production exports should require `false`. |

Each activity includes a small common envelope:

Expand All @@ -66,6 +105,8 @@ Each activity includes a small common envelope:

Activity-specific details appear when relevant. For example, click activities can include `pointer`, typing activities can include `value`, keyboard activities can include `keyboard`, navigation activities can include `navigation`, and error activities can include `error`.

Sessions that predate browser-interaction capture can return an empty `events` array.

### Target Details

When Steel can identify the element involved in an activity, `target` can include readable labels and useful selectors:
Expand All @@ -87,28 +128,38 @@ When Steel can identify the element involved in an activity, `target` can includ

Use target details as hints for review or replay. Some activities, such as navigation or errors, may not include a target.

### Filtering by Time
### Query Parameters

| Query param | Notes |
| ----------- | ----- |
| `namespace` | Limit results to a session namespace. |
| `startTime` | Include activities at or after this ISO 8601 timestamp. |
| `endTime` | Include activities at or before this ISO 8601 timestamp. |
| `limit` | Number of activities per page, from 1 to 2,000. Defaults to 500. |
| `cursor` | Opaque cursor returned as `nextCursor`. Reuse it with identical filters. |

You can limit results to a time range:
Filter by time and request a page:

```bash !! curl -wcn
curl "https://api.steel.dev/v1/sessions/$SESSION_ID/agent-traces?startTime=2026-05-22T18:00:00.000Z&endTime=2026-05-22T18:10:00.000Z" \
curl "https://api.steel.dev/v1/sessions/$SESSION_ID/agent-traces?startTime=2026-05-22T18:00:00.000Z&endTime=2026-05-22T18:10:00.000Z&limit=500" \
-H "steel-api-key: $STEEL_API_KEY"
```

| Query param | Notes |
| ----------- | ----- |
| `startTime` | Include activities at or after this ISO timestamp. |
| `endTime` | Include activities at or before this ISO timestamp. |
For a finished session, keep the filters fixed and pass each `nextCursor` into the next request until it is `null`. Paginating a live session is not a stable snapshot because new activities can arrive between requests.

Synchronous trace export accepts up to 50,000 retained raw events and a 32 MiB normalized artifact. If a full-session request returns `413`, divide the session into non-overlapping `startTime`/`endTime` windows and export each window separately.

### Errors

| Status | Meaning |
| ------ | ------- |
| 401 | Missing or invalid `steel-api-key`. |
| 403 | The session belongs to a different organization. |
| 404 | No session with that ID. |
| 404 | No accessible session with that ID. Steel also uses 404 for project or organization scope mismatches. |
| 409 | The released session's audit data is still finalizing. Retry with backoff. |
| 410 | The audit data has expired under the plan's retention period. |
| 413 | The requested time range exceeds synchronous trace export limits. Retry with narrower time bounds. |
| 429 | Rate limit hit. Retry after the `Retry-After` header. |
| 502 | The log service or browser machine is temporarily unavailable. Retry with backoff. |

:::callout
type: help
Expand Down
2 changes: 2 additions & 0 deletions content/docs/overview/sessions-api/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"overview",
"quickstart",
"session-lifecycle",
"session-logs",
"session-audit-export",
"mobile-mode",
"fullscreen-mode",
"multi-region",
Expand Down
187 changes: 187 additions & 0 deletions content/docs/overview/sessions-api/session-audit-export.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
---
title: Session Audit Export
sidebarTitle: Audit Export
description: Export session metadata, finalized cost, browser logs, and agent traces after a Steel session ends.
llm: true
publishedAt: "2026-07-23"
---

A session audit export is an application-owned snapshot of one completed run. Wait for the session to finish, capture its final details and cost, drain browser logs and agent traces, then write three versioned datasets to storage you control.

:::callout
type: warning
### Not a compliance audit log
This export is **not** a tamper-evident compliance audit log. Steel does not sign the files, chain their hashes, or prove that an exported event was never changed or omitted. If your policy requires immutable, independently verifiable records, send the export into a purpose-built WORM or compliance logging system and add your own integrity controls.
:::

### Post-session workflow

1. End the session with `sessions.release()` or wait for it to end naturally.
2. Poll session details until `status` is `released` or `failed`.
3. Request the finalized cost, browser logs, and agent traces. A `409` from any endpoint means the data is still finalizing; retry with backoff.
4. Paginate [browser logs](/overview/sessions-api/session-logs) and [agent traces](/overview/agent-traces/api) with fixed filters until `nextCursor` is `null`.
5. Persist these three datasets:
- `session.json` — export metadata, final session details, and finalized cost.
- `browser-logs.jsonl` — one versioned browser log record per line.
- `agent-traces.jsonl` — one versioned agent activity per line.

Do not begin an audit export while the session is live. New events can shift later pages and produce gaps or duplicates.

:::callout
type: info
### SDK availability
The example targets the upcoming Node SDK release that adds `sessions.logs()`, `sessions.traces()`, and `sessions.cost()`.
:::

### Example

```typescript !! Typescript -wcn
import Steel from 'steel-sdk';
import { mkdir, writeFile } from 'node:fs/promises';

const client = new Steel();
const schemaVersion = '1.0';
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function waitForAuditReady(sessionId: string) {
const deadline = Date.now() + 60_000;

while (Date.now() < deadline) {
const session = await client.sessions.retrieve(sessionId);

if (session.status !== 'live') {
try {
const cost = await client.sessions.cost(sessionId);
return { session, cost };
} catch (error) {
if (!(error instanceof Steel.APIError) || error.status !== 409) throw error;
}
}

await sleep(1_000);
}

throw new Error(`Session ${sessionId} did not become audit-ready`);
}

async function collectPages(
fetchPage: (cursor?: string) => Promise<{
events: unknown[];
hasMore: boolean;
nextCursor: string | null;
}>,
) {
const events: unknown[] = [];
let cursor: string | undefined;

while (true) {
let page;
try {
page = await fetchPage(cursor);
} catch (error) {
if (error instanceof Steel.APIError && error.status === 409) {
await sleep(1_000);
continue;
}
throw error;
}
events.push(...page.events);

if (!page.nextCursor) {
if (page.hasMore) throw new Error('Pagination stalled without a next cursor');
return events;
}
if (page.events.length === 0) throw new Error('Pagination stalled');

cursor = page.nextCursor;
}
}

type Dataset = 'browser-log' | 'agent-trace';
type RedactionPolicy = {
version: string;
apply: (dataset: Dataset | 'session', value: unknown) => unknown;
};

function toJsonLines(
dataset: Dataset,
sessionId: string,
events: unknown[],
redaction: RedactionPolicy,
) {
const lines = events.map((event) =>
JSON.stringify({
schemaVersion,
dataset,
sessionId,
event: redaction.apply(dataset, event),
}),
);

return lines.length > 0 ? `${lines.join('\n')}\n` : '';
}

async function exportSessionAudit(
sessionId: string,
outputDirectory: string,
redaction: RedactionPolicy,
) {
const { session, cost } = await waitForAuditReady(sessionId);
const [browserLogs, agentTraces] = await Promise.all([
collectPages((cursor) => client.sessions.logs(sessionId, { limit: 1_000, cursor })),
collectPages((cursor) => client.sessions.traces(sessionId, { limit: 1_000, cursor })),
]);

const manifest = {
schemaVersion,
exportedAt: new Date().toISOString(),
redactionPolicyVersion: redaction.version,
session: redaction.apply('session', session),
cost,
counts: {
browserLogs: browserLogs.length,
agentTraces: agentTraces.length,
},
};

await mkdir(outputDirectory, { recursive: true });
await Promise.all([
writeFile(`${outputDirectory}/session.json`, `${JSON.stringify(manifest, null, 2)}\n`),
writeFile(
`${outputDirectory}/browser-logs.jsonl`,
toJsonLines('browser-log', sessionId, browserLogs, redaction),
),
writeFile(
`${outputDirectory}/agent-traces.jsonl`,
toJsonLines('agent-trace', sessionId, agentTraces, redaction),
),
]);
}
```

Pass a redaction policy that handles your application's secrets and personal data. In production, retry `429` and transient `5xx` responses with bounded exponential backoff and fail the export if any page cannot be persisted.

If Agent Traces returns `413`, export the session in non-overlapping time windows. Each window must remain below the documented synchronous trace limits; persist the window bounds in `session.json` so consumers can verify complete coverage.

### Schema and versioning

Each paginated Steel response includes its own `schemaVersion`. The example also versions the application-owned export envelope separately. Keep upstream objects nested under `session`, `cost`, or `event` so additive Steel fields do not require a migration. Increment your export's major version when file names, envelope fields, or field meanings change; increment the minor version for backward-compatible additions. Consumers should reject unsupported major versions rather than silently dropping evidence.

Record the SDK version, export code version, and redaction policy version in `session.json` if you need reproducible exports.

### Redaction

Agent Traces automatically mark known-sensitive input fields, such as passwords and credit-card inputs, as redacted. That is not a complete data-loss-prevention boundary. URLs, page text, console output, request metadata, and ordinary form fields can still contain tokens or personal data.

Apply redaction before writing any dataset, test it against representative events, and restrict access to both exports and encryption keys. If you retain an original without redaction, isolate it under a stricter policy and identify sanitized copies as derived data.

### Retention

Export promptly after the session ends. Source data remains available only for your organization's current retention entitlement, which can differ for custom and grandfathered plans. Confirm that value in your Steel plan or contract rather than hard-coding a plan name. Your copies are outside Steel's retention lifecycle, so define separate expiration and deletion rules for the destination. Deleting or expiring data in Steel does not remove an export you already persisted.

### Pagination checks

- Use `nextCursor` until it is `null`. Do not construct or modify cursors.
- Keep `namespace`, time bounds, and other filters identical across pages; cursors are scoped to those filters.
- Treat `hasMore: true` without a `nextCursor`, or an empty page with `hasMore: true`, as an error.
- Store the final counts and alert if a retry produces different counts for the same completed session.
Loading
Loading