---
title: Analytics
description: Metadata-only read APIs for runs, steps, events, hooks, waits, and attributes, backed by the observability pipeline.
type: reference
summary: Interfaces: world.analytics.runs, .attributes, .steps, .events, .hooks, .waits. Metadata-only listings with plan-based lookback windows; filter runs by attribute key=value. Page limits are 1000 run-scoped, 100 cross-run.
keywords:
  - world.analytics
  - analytics.runs
  - analytics.attributes
  - attribute filter
  - getMany
  - pagination limit
  - lookback window
  - observability-upgrade-required
  - pageInfo
  - metadata-only
prerequisites:
  - /docs/api-reference/workflow-runtime/get-world
related:
  - /docs/api-reference/workflow-runtime/world/storage
  - /docs/observability/attributes
---

# Analytics



`world.analytics` is an optional, read-only namespace for observability surfaces: dashboards, command-line interface (CLI) tools, and admin tools that list large numbers of runs without touching payload data.

Prefer this namespace for observability: listing, filtering, and inspecting
workflow state. Use the [Storage](/docs/api-reference/workflow-runtime/world/storage)
API for payload-bearing reads, and for anything operational that has to see the
canonical, up-to-the-moment record.

It differs from [Storage](/docs/api-reference/workflow-runtime/world/storage) in two ways:

* **Metadata only**: Results never include run input/output, step data, or hook tokens. There is no `resolveData` option.
* **Served from the observability pipeline**: On Vercel, queries are served from the Vercel observability data pipeline, so large listings do not compete with workflow execution. Data is ingested asynchronously and may trail the live state by a few seconds.

The namespace is optional: worlds that don't implement it (such as the local development world) leave it `undefined`, so feature-detect before use:

```typescript lineNumbers
import { getWorld } from "workflow/runtime";

const world = await getWorld();
if (world.analytics) { // [!code highlight]
  const page = await world.analytics.runs.list();
}
```

***

## analytics.runs

### runs.list()

List runs with metadata, current status, and attributes. Without an explicit time window, the listing defaults to the trailing 24 hours; pass `startTime`/`endTime` to reach older runs within your plan window.

```typescript lineNumbers
const page = await world.analytics.runs.list({
  workflowName: "orderWorkflow",
  status: "failed",
  attributes: { source: "checkout" }, // [!code highlight]
  pagination: { limit: 50, sortOrder: "desc" },
});
```

| Parameter                             | Type                     | Description                                                         |
| ------------------------------------- | ------------------------ | ------------------------------------------------------------------- |
| `params.workflowName`                 | `string`                 | Filter to one workflow                                              |
| `params.status`                       | `string`                 | `pending`, `running`, `completed`, `failed`, or `cancelled`         |
| `params.startTime` / `params.endTime` | `string`                 | ISO 8601 window; must be provided together                          |
| `params.attributes`                   | `Record<string, string>` | Only return runs whose latest attributes match every pair (up to 8) |
| `params.pagination`                   | `PaginationOptions`      | Cursor pagination                                                   |

**Returns:** `PaginatedResponse<AnalyticsRun>`. Each run includes `runId`, `status`, `workflowName`, `deploymentId`, `attributes`, and lifecycle timestamps.

Attribute matching is latest-write-wins: a run whose attribute moved from `"v1"` to `"v2"` no longer matches `{ key: "v1" }`. Reserved `$`-prefixed keys may be used in filters even though user code cannot write them.

### runs.get()

Fetch one run by ID. Point lookups search the full plan window rather than only the trailing 24 hours.

```typescript lineNumbers
const run = await world.analytics.runs.get(runId);
```

***

## analytics.attributes

Discover which [attributes](/docs/observability/attributes) exist on your runs, for example to build filter dropdowns over arbitrary user-defined keys.

### attributes.list()

List the distinct attribute keys observed on runs in the window, ordered alphabetically.

```typescript lineNumbers
const page = await world.analytics.attributes.list({ // [!code highlight]
  workflowName: "orderWorkflow",
});
for (const { key, runCount, lastSeenAt } of page.data) {
  console.log(key, runCount, lastSeenAt);
}
```

| Parameter                             | Type                | Description                                |
| ------------------------------------- | ------------------- | ------------------------------------------ |
| `params.workflowName`                 | `string`            | Only count runs of one workflow            |
| `params.startTime` / `params.endTime` | `string`            | ISO 8601 window; must be provided together |
| `params.pagination`                   | `PaginationOptions` | Cursor pagination                          |

**Returns:** `PaginatedResponse<AnalyticsAttributeKey>`: `{ key, runCount, firstSeenAt, lastSeenAt }`

***

## analytics.steps

Run-scoped step listings mirroring their [Storage](/docs/api-reference/workflow-runtime/world/storage) counterparts, minus payload data.

### steps.list()

```typescript lineNumbers
const steps = await world.analytics.steps.list({
  runId,
  pagination: { limit: 200, sortOrder: "asc" },
});
```

| Parameter           | Type                | Description                           |
| ------------------- | ------------------- | ------------------------------------- |
| `params.runId`      | `string`            | Required. The run to list steps for   |
| `params.pagination` | `PaginationOptions` | Cursor pagination, `limit` up to 1000 |

**Returns:** `PaginatedResponse<AnalyticsStep>`. Each step includes `stepId`, `stepName`, `status`, `attempt`, lifecycle timestamps, `errorCode`, and the `computeInstanceId` of the latest attempt.

### steps.get()

```typescript lineNumbers
const step = await world.analytics.steps.get(runId, stepId);
```

**Returns:** `AnalyticsStep`. A step id is only unique within its run, so both arguments are required.

***

## analytics.events

### events.list()

```typescript lineNumbers
const events = await world.analytics.events.list({
  runId,
  eventType: "step_failed", // [!code highlight]
  pagination: { limit: 1000 },
});
```

| Parameter              | Type                | Description                                                 |
| ---------------------- | ------------------- | ----------------------------------------------------------- |
| `params.runId`         | `string`            | Required. The run to list events for                        |
| `params.eventType`     | `string`            | One event type, for example `run_failed` or `step_retrying` |
| `params.correlationId` | `string`            | Narrow to one entity: a step, hook, wait, or attribute id   |
| `params.pagination`    | `PaginationOptions` | Cursor pagination, `limit` up to 1000                       |

**Returns:** `PaginatedResponse<AnalyticsEvent>`. Each event includes `eventId`, `eventType`, `correlationId`, `stepName`, `createdAt`, and provenance fields (`region`, `requestId`, `computeInstanceId`).

Pass a step id as `correlationId` to build that step's timeline: `step_created` through `step_completed`, `step_failed`, or `step_retrying`.

### events.get()

```typescript lineNumbers
const event = await world.analytics.events.get(runId, eventId);
```

**Returns:** `AnalyticsEvent`.

### events.getMany()

Look up a bounded set of event ids in one run with a single request.

```typescript lineNumbers
const events = await world.analytics.events.getMany(runId, eventIds); // [!code highlight]
```

**Returns:** `AnalyticsEvent[]` — not paginated, and no `pageInfo`. Duplicate ids are looked up once, and ids with no analytics row yet are **omitted rather than erroring**, since ingestion can trail canonical storage. Compare the returned length against your input to detect that.

***

## analytics.hooks

### hooks.list()

```typescript lineNumbers
const hooks = await world.analytics.hooks.list({ runId });
```

| Parameter           | Type                | Description                          |
| ------------------- | ------------------- | ------------------------------------ |
| `params.runId`      | `string`            | Required. The run to list hooks for  |
| `params.pagination` | `PaginationOptions` | Cursor pagination, `limit` up to 100 |

**Returns:** `PaginatedResponse<AnalyticsHook>`: `hookId`, `status` (`created`, `received`, `disposed`, or `conflict`), `receivedAt`, `disposedAt`, `isWebhook`, `isSystem`.

### hooks.get()

```typescript lineNumbers
const hook = await world.analytics.hooks.get(hookId);
```

Unlike steps and waits, a hook id identifies one hook on its own, so no `runId` is needed. Pass `{ runId }` to scope the lookup when you already know it.

<Callout>
  Hook listings never include the hook token. Resolve it separately through the
  runtime APIs if you need to deliver a payload.
</Callout>

***

## analytics.waits

### waits.list()

```typescript lineNumbers
const waits = await world.analytics.waits.list({
  runId,
  status: "waiting", // [!code highlight]
});
```

| Parameter           | Type                | Description                           |
| ------------------- | ------------------- | ------------------------------------- |
| `params.runId`      | `string`            | Required. The run to list waits for   |
| `params.status`     | `string`            | `waiting` or `completed`              |
| `params.pagination` | `PaginationOptions` | Cursor pagination, `limit` up to 1000 |

**Returns:** `PaginatedResponse<AnalyticsWait>`: `waitId`, `status`, `resumeAt`, `completedAt`.

### waits.get()

```typescript lineNumbers
const wait = await world.analytics.waits.get(runId, waitId);
```

**Returns:** `AnalyticsWait`. A wait id is only unique within its run, so both arguments are required.

***

## Limits and validation

Arguments are validated in your process before a request goes out. An
out-of-range or malformed argument throws a `RangeError` naming the bound it
broke, rather than reaching the backend and coming back as a 400 — which
matters because analytics is optional and callers commonly wrap it in a
`try`/`catch`, where a rejected request is easy to mistake for "no data".

### Page limits

`pagination.limit` defaults to 40 everywhere. The maximum depends on whether
the listing scans within one run or across runs:

| Method                                             | Max `limit` |
| -------------------------------------------------- | ----------- |
| `steps.list()`, `events.list()`, `waits.list()`    | 1000        |
| `runs.list()`, `attributes.list()`, `hooks.list()` | 100         |

`events.getMany()` is not paginated; it accepts 1 to 100 event ids per call.

<Callout type="warn">
  The two page caps differ by a factor of ten, and `hooks.list()` takes the
  lower one despite being run-scoped. Reusing one page size across listings is
  the most common way to trip this.
</Callout>

### Identifiers

Every id is a prefix plus a ULID, and each is checked before the request:

| Parameter       | Shape                                 |
| --------------- | ------------------------------------- |
| `runId`         | `wrun_`                               |
| `stepId`        | `step_`                               |
| `eventId`       | `evnt_`                               |
| `hookId`        | `hook_`                               |
| `waitId`        | `wait_`                               |
| `correlationId` | `step_`, `hook_`, `wait_`, or `attr_` |

Step, event, and wait ids are only unique **within** their run, so the methods that take them require a `runId` too. A hook id stands alone.

### Time windows

`startTime` and `endTime` must be supplied **together** and be parseable ISO 8601 timestamps with `startTime` no later than `endTime`. Passing one without the other throws: it used to be dropped silently, which turned a listing you meant to bound into a scan of the whole retention window that looked like a successful answer.

### Attribute filters

`runs.list({ attributes })` accepts 1 to 8 pairs. Keys are 1 to 256 characters; values are at most 256 UTF-8 bytes. Reserved `$`-prefixed keys are valid in a filter even though user code cannot write them.

### Pagination

`cursor` is an opaque token from the previous response; do not construct or parse one. Branch on `hasMore`, not on `cursor` being non-null, and do not change `sortOrder` mid-walk — the cursor encodes the sort position, so reversing it can skip or repeat rows.

***

## Lookback windows and pageInfo

Every paginated response carries `pageInfo` describing the window the query was allowed to scan:

{/* @skip-typecheck: shape illustration, not runnable code */}

```typescript
{
  currentLookbackDays: 2,     // what your plan allows today
  maxLookbackDays: 30,        // ceiling with Observability Plus on Vercel
  currentWindowStart: Date,
  maxWindowStart: Date,
  upgradeAvailable: true,     // for Vercel deployed workflows
}
```

Requests for a window older than `currentWindowStart` fail with an `observability-upgrade-required` error; windows older than `maxWindowStart` return not-found. Use `pageInfo` to size date pickers and to decide whether to surface an upgrade prompt.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)