workflow
Version:
Workflow SDK - Build durable, resilient, and observable workflows
316 lines (223 loc) • 12 kB
text/mdx
---
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."
prerequisites:
- /docs/api-reference/workflow-runtime/get-world
related:
- /docs/api-reference/workflow-runtime/world/storage
- /docs/observability/attributes
keywords:
- world.analytics
- analytics.runs
- analytics.attributes
- attribute filter
- getMany
- pagination limit
- lookback window
- observability-upgrade-required
- pageInfo
- metadata-only
---
`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.