Attributes

setAttributes attaches plaintext string metadata to the current workflow run. These attributes appear in the Workflow CLI and web UI, and you can search and filter runs by them from either the CLI or the Analytics API.

You can also seed any attributes directly when starting a run:

const run = await start(orderWorkflow, ["ord_123"], {
  attributes: { source: "checkout" },
})
import { setAttributes } from "workflow"

export async function orderWorkflow(orderId: string) {
  "use workflow"

  await setAttributes({
    phase: "received",
    orderId,
  }) 

  // ...work...

  await setAttributes({ phase: "complete" }) 
}

Usage

Call setAttributes from a "use workflow" function or a "use step" function. Plain application code is not supported because there is no active workflow run to attach attributes to.

Values must be strings. Pass undefined to remove a key:

import { setAttributes } from "workflow"

export async function cleanupAttributes() {
  "use workflow"

  await setAttributes({ staleKey: undefined }) 
}

Attribute keys must be 1-256 characters, values must be strings up to 256 bytes, and each run can have up to 64 attributes. Keys that start with $ are reserved for framework and library code.

Each setAttributes call also writes one attr_set event, and that event's complete data (keys, values, and writer metadata) must fit in 8192 UTF-8 JSON bytes (8KiB). Split large updates across several calls; the calls still share the 64-attribute limit. A call that exceeds a limit rejects with FatalError before anything is written, so catch it when the metadata is best-effort.

Reserved keys

When start() is called from inside a running workflow or step, the new run is automatically tagged with two reserved attributes:

  • $parentRunId: the run that started it.
  • $rootRunId: the root of the chain. It is inherited, so every run in a daisy chain or fan-out shares one root id.

Top-level runs (started outside any workflow or step) are not tagged.

Viewing attributes

The run details panel in the observability UI shows the run's current attributes as key-value rows. Reserved $-prefixed keys are marked with a badge and sorted after user keys:

Run details panel showing the Attributes card with reserved keys badged

Each setAttributes call appears on the trace timeline as a diamond marker at the moment the attributes were written:

Trace timeline with attr_set diamond markers on the run row

Expanding an attr_set event (in the run sidebar or the Events tab) shows the changed keys, removed keys, and whether the write came from the workflow body or a step (with the attempt number):

Expanded attr_set events showing changes and the writer

Searching and filtering by attributes

From the CLI

workflow inspect attributes lists the keys recorded on this project's runs, with how many runs carry each and when it was first and last seen:

workflow inspect attributes

Pass one or more --attribute key=value pairs to inspect runs to list the runs carrying them. Repeatable up to 8 times:

workflow inspect runs --attribute phase=received --status running

Both require a backend with the analytics read path. --attribute is ignored with a warning on backends without one, and inspect attributes reports that it is unavailable.

From the Analytics API

The Analytics API can discover which attribute keys exist and filter run listings by them. The analytics namespace is optional on World, so feature-detect it before use; it is absent on local, Postgres, and other custom Worlds:

import { getWorld } from "workflow/runtime";

const world = await getWorld();
if (!world.analytics) {
  throw new Error("This World does not support analytics queries");
}

// Which attribute keys exist, and on how many runs?
const keys = await world.analytics.attributes.list();

// List runs whose latest attributes match every pair
const stuck = await world.analytics.runs.list({
  attributes: { phase: "received" },
});

Matching is latest-write-wins: once the run above writes phase: "complete", it stops matching phase: "received".

Behavior

  • Attributes require a World implementing spec version 4 or later.
  • Writes from workflow and step bodies append native attr_set events and immediately materialize run.attributes.
  • Storage errors surface rather than being silently ignored: transient errors on workflow-body writes are retried, and a write the World rejects as invalid (for example, exceeding the per-run attribute cap across multiple calls) fails the run with the validation error.
  • Step-body storage errors throw from setAttributes like any other step-side network write. Catch the error inside the step if the attribute is best-effort.
  • Reading and querying: each run's current attributes are returned on the run objects from the Storage and Analytics APIs, and the Analytics API supports discovering attribute keys and filtering run listings by key=value pairs (see Searching and filtering by attributes). On Worlds without the optional analytics namespace, attributes are readable on run objects but not searchable.