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 API for payload-bearing reads, and for anything operational that has to see the canonical, up-to-the-moment record.

It differs from 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:

import { getWorld } from "workflow/runtime";

const world = await getWorld();
if (world.analytics) {
  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.

const page = await world.analytics.runs.list({
  workflowName: "orderWorkflow",
  status: "failed",
  attributes: { source: "checkout" },
  pagination: { limit: 50, sortOrder: "desc" },
});
ParameterTypeDescription
params.workflowNamestringFilter to one workflow
params.statusstringpending, running, completed, failed, or cancelled
params.startTime / params.endTimestringISO 8601 window; must be provided together
params.attributesRecord<string, string>Only return runs whose latest attributes match every pair (up to 8)
params.paginationPaginationOptionsCursor 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.

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

analytics.attributes

Discover which 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.

const page = await world.analytics.attributes.list({
  workflowName: "orderWorkflow",
});
for (const { key, runCount, lastSeenAt } of page.data) {
  console.log(key, runCount, lastSeenAt);
}
ParameterTypeDescription
params.workflowNamestringOnly count runs of one workflow
params.startTime / params.endTimestringISO 8601 window; must be provided together
params.paginationPaginationOptionsCursor pagination

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


analytics.steps

Run-scoped step listings mirroring their Storage counterparts, minus payload data.

steps.list()

const steps = await world.analytics.steps.list({
  runId,
  pagination: { limit: 200, sortOrder: "asc" },
});
ParameterTypeDescription
params.runIdstringRequired. The run to list steps for
params.paginationPaginationOptionsCursor 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()

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()

const events = await world.analytics.events.list({
  runId,
  eventType: "step_failed",
  pagination: { limit: 1000 },
});
ParameterTypeDescription
params.runIdstringRequired. The run to list events for
params.eventTypestringOne event type, for example run_failed or step_retrying
params.correlationIdstringNarrow to one entity: a step, hook, wait, or attribute id
params.paginationPaginationOptionsCursor 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()

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.

const events = await world.analytics.events.getMany(runId, eventIds);

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()

const hooks = await world.analytics.hooks.list({ runId });
ParameterTypeDescription
params.runIdstringRequired. The run to list hooks for
params.paginationPaginationOptionsCursor pagination, limit up to 100

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

hooks.get()

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.

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


analytics.waits

waits.list()

const waits = await world.analytics.waits.list({
  runId,
  status: "waiting",
});
ParameterTypeDescription
params.runIdstringRequired. The run to list waits for
params.statusstringwaiting or completed
params.paginationPaginationOptionsCursor pagination, limit up to 1000

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

waits.get()

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:

MethodMax 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.

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.

Identifiers

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

ParameterShape
runIdwrun_
stepIdstep_
eventIdevnt_
hookIdhook_
waitIdwait_
correlationIdstep_, 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:

{
  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.