From b6ce358029b992989a830e4295693741d7ec64e3 Mon Sep 17 00:00:00 2001 From: Pranay Prakash Date: Tue, 5 May 2026 12:49:32 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20workflow=20attributes=20(V1)=20?= =?UTF-8?q?=E2=80=94=20plan=20+=20scaffolding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opens a draft PR for plan + skipped e2e tests + docs review. No implementation yet. Full plan in PR body. Co-Authored-By: Claude Opus 4.7 (1M context) From 17173176d985367168948f5c330aa53fd7045ea9 Mon Sep 17 00:00:00 2001 From: Pranay Prakash Date: Tue, 5 May 2026 20:26:37 +0900 Subject: [PATCH 2/3] docs+test: scaffolding for workflow attributes (V1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the docs and skipped e2e tests for review *before* implementation. No runtime changes — all four function shims (setAttribute, setAttributes, getAttribute, getAttributes), the runs.list filter, and the keys/values enumeration endpoints are referenced as if they exist, with the necessary ambient declarations and `// @setup` blocks to keep the docs typecheck and the workspace typecheck green. Docs: - docs/content/docs/foundations/attributes.mdx — concept guide. - docs/content/docs/api-reference/workflow/{set-attribute,set-attributes, get-attribute,get-attributes}.mdx — API reference for each export. - Wires the new pages into the foundations + api-reference index Cards and meta.json. - packages/docs-typecheck/src/docs-globals.d.ts — temporary ambient declarations marked `TODO(attributes V1)` for the unimplemented APIs so the docs samples typecheck. Tests: - workbench/example/workflows/9_attributes.ts — workflow + step that drive every code path the e2e suite asserts on. - workbench/{nextjs-turbopack,nextjs-webpack,nitro-v3}/workflows/ 9_attributes.ts — the standard symlink fan-out. - packages/core/e2e/attributes.test.ts — full describe.skip suite covering: round-trip, batch single-event, start({attributes}) initial state, writer attribution (workflow vs step + attempt), retry attribution, replay determinism, runs.list AND-filter, and listAttributeKeys / listAttributeValues with prefix. Each `as any` / `as unknown as ...` and ambient shim carries a `TODO(attributes)` marker so the cleanup is greppable when the implementation lands. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api-reference/workflow/get-attribute.mdx | 82 ++++ .../api-reference/workflow/get-attributes.mdx | 80 ++++ .../docs/api-reference/workflow/index.mdx | 12 + .../api-reference/workflow/set-attribute.mdx | 100 +++++ .../api-reference/workflow/set-attributes.mdx | 114 +++++ docs/content/docs/foundations/attributes.mdx | 209 +++++++++ docs/content/docs/foundations/index.mdx | 3 + docs/content/docs/foundations/meta.json | 3 +- packages/core/e2e/attributes.test.ts | 409 ++++++++++++++++++ packages/docs-typecheck/src/docs-globals.d.ts | 21 + workbench/example/workflows/9_attributes.ts | 122 ++++++ .../workflows/9_attributes.ts | 1 + .../nextjs-webpack/workflows/9_attributes.ts | 1 + workbench/nitro-v3/workflows/9_attributes.ts | 1 + 14 files changed, 1157 insertions(+), 1 deletion(-) create mode 100644 docs/content/docs/api-reference/workflow/get-attribute.mdx create mode 100644 docs/content/docs/api-reference/workflow/get-attributes.mdx create mode 100644 docs/content/docs/api-reference/workflow/set-attribute.mdx create mode 100644 docs/content/docs/api-reference/workflow/set-attributes.mdx create mode 100644 docs/content/docs/foundations/attributes.mdx create mode 100644 packages/core/e2e/attributes.test.ts create mode 100644 workbench/example/workflows/9_attributes.ts create mode 120000 workbench/nextjs-turbopack/workflows/9_attributes.ts create mode 120000 workbench/nextjs-webpack/workflows/9_attributes.ts create mode 120000 workbench/nitro-v3/workflows/9_attributes.ts diff --git a/docs/content/docs/api-reference/workflow/get-attribute.mdx b/docs/content/docs/api-reference/workflow/get-attribute.mdx new file mode 100644 index 0000000000..1483f442bd --- /dev/null +++ b/docs/content/docs/api-reference/workflow/get-attribute.mdx @@ -0,0 +1,82 @@ +--- +title: getAttribute +description: Read a single attribute from the current workflow run. +type: reference +summary: Use getAttribute to look up a key on the current run's attribute snapshot. +prerequisites: + - /docs/foundations/workflows-and-steps + - /docs/foundations/attributes +related: + - /docs/foundations/attributes + - /docs/api-reference/workflow/get-attributes + - /docs/api-reference/workflow/set-attribute +--- + + + Attributes are a V1 preview feature. The API is stable but the + observability/filter UI is still rolling out. + + +Returns the current value of a single attribute on the workflow run, or `undefined` if the key is not set. Synchronous — `getAttribute` reads from the in-memory snapshot the runtime maintains for the run; it does not hit the network. + +```typescript lineNumbers +import { getAttribute, setAttribute } from "workflow" + +async function exampleWorkflow() { + "use workflow" + await setAttribute("phase", "init") + const phase = getAttribute("phase") // "init" // [!code highlight] +} +``` + +## Read-your-writes + +Within the same workflow or step body, `getAttribute` reflects any writes you have already issued, even if the underlying event has not yet round-tripped to the server. + +```typescript lineNumbers +import { getAttribute, setAttribute } from "workflow" + +async function readStep() { + "use step" + await setAttribute("orderId", "ord_1") + const value = getAttribute("orderId") // "ord_1" — local update is visible +} +``` + +## Reading initial attributes from `start()` + +If the run was started with [`start(workflow, input, { attributes })`](/docs/foundations/starting-workflows), those attributes are visible to `getAttribute` from the very first line of the workflow body: + +```typescript lineNumbers +import { getAttribute } from "workflow" +import { start } from "workflow/api" +declare const order: any; // @setup +declare const myWorkflow: (order: any) => Promise; // @setup + +// caller: +await start(myWorkflow, [order], { attributes: { tenantId: "t_acme" } } as any) +// TODO(attributes V1): drop the cast once StartOptions accepts attributes. + +// inside myWorkflow: +async function myWorkflowImpl(order: any) { + "use workflow" + const tenantId = getAttribute("tenantId") // "t_acme" // [!code highlight] +} +``` + +## Workflow context vs step context + +`getAttribute` works in both contexts. The values are eventually consistent: + +- Inside a **step**, the step sees the run's attribute snapshot at step start, plus any of its own writes (read-your-writes). +- Inside a **workflow** body, the workflow sees the snapshot up to the most-recent observed event. While the workflow is suspended waiting on a step, the workflow does not observe attribute writes the step is doing — those become visible once the step completes and the workflow resumes. + +## Errors + +`getAttribute` throws if called outside any workflow or step context. Otherwise it never throws — unset keys simply return `undefined`. + +## See also + +- [Attributes guide](/docs/foundations/attributes) +- [`getAttributes`](/docs/api-reference/workflow/get-attributes) — read all attributes at once. +- [`setAttribute`](/docs/api-reference/workflow/set-attribute) — write a single attribute. diff --git a/docs/content/docs/api-reference/workflow/get-attributes.mdx b/docs/content/docs/api-reference/workflow/get-attributes.mdx new file mode 100644 index 0000000000..cf71d87e42 --- /dev/null +++ b/docs/content/docs/api-reference/workflow/get-attributes.mdx @@ -0,0 +1,80 @@ +--- +title: getAttributes +description: Read all attributes on the current workflow run as a record. +type: reference +summary: Use getAttributes to retrieve the full attribute snapshot for the current run. +prerequisites: + - /docs/foundations/workflows-and-steps + - /docs/foundations/attributes +related: + - /docs/foundations/attributes + - /docs/api-reference/workflow/get-attribute + - /docs/api-reference/workflow/set-attributes +--- + + + Attributes are a V1 preview feature. The API is stable but the + observability/filter UI is still rolling out. + + +Returns the full set of attributes on the current workflow run as a `Record`. The returned object is a snapshot — mutating it has no effect on the run. + +```typescript lineNumbers +import { getAttributes, setAttribute } from "workflow" + +async function exampleWorkflow() { + "use workflow" + await setAttribute("phase", "init") + await setAttribute("orderId", "ord_1") + + const attrs = getAttributes() // [!code highlight] + // { phase: "init", orderId: "ord_1" } +} +``` + +`getAttributes` is the bulk equivalent of [`getAttribute`](/docs/api-reference/workflow/get-attribute). Use it when you need to inspect or log the full state — for example, attaching the full attribute set to a log line: + +```typescript lineNumbers +import { getAttributes, getWorkflowMetadata } from "workflow" + +function logWithAttributes(message: string) { + const { workflowRunId } = getWorkflowMetadata() + const attrs = getAttributes() + console.log(`[${workflowRunId}]`, message, attrs) +} +``` + +## Read-your-writes + +Inside a single workflow or step body, `getAttributes` reflects any prior `setAttribute` / `setAttributes` calls in that body, including unsets: + +```typescript lineNumbers +import { getAttributes, setAttribute, setAttributes } from "workflow" + +async function step() { + "use step" + await setAttributes({ a: "1", b: "2" }) + await setAttribute("a", undefined) + + getAttributes() // { b: "2" } — `a` was unset +} +``` + +## When `getAttributes` is empty + +`getAttributes()` returns `{}` (an empty record) when: + +- The run was started without `start({ attributes })`, and +- No `setAttribute` / `setAttributes` calls have been made. + +It never returns `undefined`. + +## Errors + +`getAttributes` throws if called outside any workflow or step context. + +## See also + +- [Attributes guide](/docs/foundations/attributes) +- [`getAttribute`](/docs/api-reference/workflow/get-attribute) — read a single attribute. +- [`setAttributes`](/docs/api-reference/workflow/set-attributes) — write many attributes at once. diff --git a/docs/content/docs/api-reference/workflow/index.mdx b/docs/content/docs/api-reference/workflow/index.mdx index 8544b47d70..540335ae9f 100644 --- a/docs/content/docs/api-reference/workflow/index.mdx +++ b/docs/content/docs/api-reference/workflow/index.mdx @@ -47,6 +47,18 @@ Workflow SDK contains the following functions you can use inside your workflow f Access the current workflow run's default stream. + + Tag a run with a single key-value attribute for observability and filtering. + + + Tag a run with multiple attributes in a single batched event. + + + Read a single attribute from the current run. + + + Read the full attribute snapshot for the current run. + ## Error Classes diff --git a/docs/content/docs/api-reference/workflow/set-attribute.mdx b/docs/content/docs/api-reference/workflow/set-attribute.mdx new file mode 100644 index 0000000000..aabcbe0eeb --- /dev/null +++ b/docs/content/docs/api-reference/workflow/set-attribute.mdx @@ -0,0 +1,100 @@ +--- +title: setAttribute +description: Set a single attribute on the current workflow run from inside a workflow or step. +type: reference +summary: Use setAttribute to record a key-value attribute on the current run for observability and filtering. +prerequisites: + - /docs/foundations/workflows-and-steps + - /docs/foundations/attributes +related: + - /docs/foundations/attributes + - /docs/api-reference/workflow/set-attributes + - /docs/api-reference/workflow/get-attribute +--- + + + Attributes are a V1 preview feature. The API is stable but the + observability/filter UI is still rolling out. + + +Sets a single key-value attribute on the current workflow run. Attributes are small, plaintext metadata that you can search, filter, and display in the observability UI without exposing step inputs/outputs. + +You can call `setAttribute` from either a workflow function or a step function. The runtime records who wrote what (workflow body vs. specific step + attempt) in the event log so observability can attribute each change to its source. + +```typescript lineNumbers +import { setAttribute } from "workflow" + +async function processOrderWorkflow(orderId: string) { + "use workflow" + await setAttribute("orderId", orderId) // [!code highlight] +} +``` + +## Unsetting an attribute + +Pass `undefined` as the value to remove a previously-set attribute: + +```typescript lineNumbers +import { setAttribute } from "workflow" + +async function checkoutWorkflow() { + "use workflow" + await setAttribute("phase", "init") + // ... later, after the work finishes: + await setAttribute("phase", undefined) // [!code highlight] +} +``` + +## Setting attributes from a step + +`setAttribute` works the same way inside a step function. The only difference is that the event log records the step's `stepId` and `attempt` number against the change, so observability can show "set by step `processOrder` (attempt 2)". + +```typescript lineNumbers +import { setAttribute } from "workflow" + +async function processOrderStep(orderId: string) { + "use step" + await setAttribute("orderId", orderId) // [!code highlight] + // ... do the actual work +} +``` + +If the step retries, the new attempt re-emits whatever `setAttribute` calls the step makes. The event log shows both attempts; the run's current attribute snapshot reflects the most-recent write. + +## Setting many attributes at once + +If you have several attributes to set at the same point in time, prefer [`setAttributes`](/docs/api-reference/workflow/set-attributes) — it batches all the changes into a single event for slightly lower overhead and a cleaner event log. + +```typescript lineNumbers +import { setAttribute, setAttributes } from "workflow" + +async function startStep() { + "use step" + // Three separate events: + await setAttribute("orderId", "ord_1") + await setAttribute("region", "us-east-1") + await setAttribute("stepKind", "process") + + // One event with three changes: + await setAttributes({ + orderId: "ord_1", + region: "us-east-1", + stepKind: "process", + }) +} +``` + +## Limits and validation + +V1 enforces these limits — violations throw [`FatalError`](/docs/api-reference/workflow/fatal-error): + +- `key`: 1–256 characters, must not start with `$` (the `$`-prefix is reserved for future system keys). +- `value`: ≤ 256 bytes UTF-8 (or `undefined` to unset). +- ≤ 64 attributes per run. + +## See also + +- [Attributes guide](/docs/foundations/attributes) — concepts, use cases, and the search/filter story. +- [`setAttributes`](/docs/api-reference/workflow/set-attributes) — batch helper. +- [`getAttribute`](/docs/api-reference/workflow/get-attribute) — read a single attribute. +- [`getAttributes`](/docs/api-reference/workflow/get-attributes) — read all attributes. diff --git a/docs/content/docs/api-reference/workflow/set-attributes.mdx b/docs/content/docs/api-reference/workflow/set-attributes.mdx new file mode 100644 index 0000000000..579c325ec4 --- /dev/null +++ b/docs/content/docs/api-reference/workflow/set-attributes.mdx @@ -0,0 +1,114 @@ +--- +title: setAttributes +description: Set multiple attributes on the current workflow run in a single batched event. +type: reference +summary: Use setAttributes to record several key-value attributes at once with merge semantics. +prerequisites: + - /docs/foundations/workflows-and-steps + - /docs/foundations/attributes +related: + - /docs/foundations/attributes + - /docs/api-reference/workflow/set-attribute + - /docs/api-reference/workflow/get-attributes +--- + + + Attributes are a V1 preview feature. The API is stable but the + observability/filter UI is still rolling out. + + +Sets multiple attributes on the current workflow run in a single batched event. Use this whenever you have several attributes to write at the same point in time — it produces one event with multiple changes, instead of one event per attribute. + +```typescript lineNumbers +import { setAttributes } from "workflow" + +async function processOrderStep(orderId: string) { + "use step" + await setAttributes({ // [!code highlight] + orderId, // [!code highlight] + region: "us-east-1", // [!code highlight] + stepKind: "process", // [!code highlight] + }) // [!code highlight] +} +``` + +## Merge semantics + +`setAttributes` is **additive**: only the keys you pass are touched. Other attributes already on the run remain unchanged. + +```typescript lineNumbers +import { setAttributes } from "workflow" + +async function fanOutStep() { + "use step" + await setAttributes({ a: "1", b: "2" }) + // run.attributes is now { a: "1", b: "2" } + + await setAttributes({ b: "two", c: "3" }) + // run.attributes is now { a: "1", b: "two", c: "3" } + // - `a` is preserved (not in the second call) + // - `b` is overwritten + // - `c` is added +} +``` + +To clear an attribute, pass `undefined` as its value — the same convention as [`setAttribute`](/docs/api-reference/workflow/set-attribute): + +```typescript lineNumbers +import { setAttributes } from "workflow" + +async function cleanupStep() { + "use step" + await setAttributes({ + phase: "done", + region: undefined, // [!code highlight] — removes `region` + }) +} +``` + +## Empty record is a no-op + +Calling `setAttributes({})` does nothing — no event is emitted. This makes it safe to compute attribute updates conditionally without special-casing the empty path. + +```typescript lineNumbers +import { setAttributes } from "workflow" + +async function maybeStep(updates: Record) { + "use step" + await setAttributes(updates) // safe even if `updates` is {} +} +``` + +## When to prefer `setAttribute` + +For one-off single-key writes, [`setAttribute`](/docs/api-reference/workflow/set-attribute) reads more naturally: + +```typescript lineNumbers +import { setAttribute, setAttributes } from "workflow" + +async function exampleWorkflow() { + "use workflow" + // Single key — prefer setAttribute: + await setAttribute("phase", "init") + + // Multiple keys at once — prefer setAttributes: + await setAttributes({ tenantId: "t_1", region: "us-east-1" }) +} +``` + +Under the hood, both produce the same event type (`attr_set`); `setAttribute(k, v)` is shorthand for `setAttributes({ [k]: v })`. + +## Limits and validation + +V1 enforces these limits across the merged result — violations throw [`FatalError`](/docs/api-reference/workflow/fatal-error): + +- Each `key`: 1–256 characters, must not start with `$` (reserved). +- Each `value`: ≤ 256 bytes UTF-8 (or `undefined` to unset). +- ≤ 64 attributes per run **after** the merge is applied. + +## See also + +- [Attributes guide](/docs/foundations/attributes) — concepts, use cases, and the search/filter story. +- [`setAttribute`](/docs/api-reference/workflow/set-attribute) — single-key write. +- [`getAttribute`](/docs/api-reference/workflow/get-attribute) — read a single attribute. +- [`getAttributes`](/docs/api-reference/workflow/get-attributes) — read all attributes. diff --git a/docs/content/docs/foundations/attributes.mdx b/docs/content/docs/foundations/attributes.mdx new file mode 100644 index 0000000000..2fbb3ab49a --- /dev/null +++ b/docs/content/docs/foundations/attributes.mdx @@ -0,0 +1,209 @@ +--- +title: Attributes +description: Attach searchable, plaintext metadata to workflow runs for observability and filtering. +type: conceptual +summary: Use attributes to label runs with business data so you can find, filter, and group them later. +prerequisites: + - /docs/foundations/workflows-and-steps + - /docs/foundations/starting-workflows +related: + - /docs/api-reference/workflow/set-attribute + - /docs/api-reference/workflow/set-attributes + - /docs/api-reference/workflow/get-attribute + - /docs/api-reference/workflow/get-attributes +--- + + + Attributes are a V1 preview feature. The API is stable; the observability + UI is still rolling out, and value types are limited to strings. + + +Attributes are small, plaintext key-value pairs you can attach to a workflow run. They're meant for the data you want to see, search, and filter on in observability — things like `tenantId`, `orderId`, `phase`, `region`, `customerEmail`. They are **not** meant for arbitrary structured data; for that, use step inputs/outputs. + +## Why attributes + +Step inputs and outputs are encrypted and live behind remote refs. That's the right call for sensitive data, but it means the observability UI can't display them inline, and the platform can't index them for search. You end up with a list of runs identified only by opaque IDs, with no way to find "all runs for `tenantId: t_acme`" without fetching each run individually. + +Attributes solve that. They're stored unencrypted on the run row itself, indexed by the platform, and surfaced directly in the observability UI. You can: + +- See which runs belong to which tenant / order / customer at a glance. +- Filter the run list by attribute key/value combinations. +- Build dashboards that group by attribute (e.g. "runs per region per day"). +- Drive support flows: paste a customer's order ID into a search box and see exactly the runs that handled their order. + +## Three ways to set attributes + +### 1. At start time + +Pass `attributes` as the third argument to `start()`. These become the run's birth state — they're visible to the workflow body from the very first line. + +```typescript lineNumbers +import { start } from "workflow/api" +declare const processOrder: (orderId: string) => Promise; // @setup +declare const orderId: string; // @setup + +await start(processOrder, [orderId], { + attributes: { tenantId: "t_acme", orderId, channel: "web" }, // [!code highlight] +} as any) // TODO(attributes V1): drop the cast once StartOptions accepts attributes. +``` + +This is the most common pattern: known-at-call-time data goes in `start()`. Use it whenever the caller knows the values. + +### 2. From inside a workflow + +Use [`setAttribute`](/docs/api-reference/workflow/set-attribute) or [`setAttributes`](/docs/api-reference/workflow/set-attributes) to record attributes the workflow itself computes: + +```typescript lineNumbers +import { setAttribute } from "workflow" +declare const doExpensiveWork: (orderId: string) => Promise; // @setup + +async function processOrder(orderId: string) { + "use workflow" + await setAttribute("phase", "init") // [!code highlight] + + const result = await doExpensiveWork(orderId) + + await setAttribute("phase", "done") // [!code highlight] + return result +} +``` + +The event log records the writer as `{ type: 'workflow' }` so observability can show which writes came from the workflow body vs. from steps. + +### 3. From inside a step + +Same API, same syntax — but the event log records the writer as `{ type: 'step', stepId, attempt }`, so you can trace each attribute change to the exact step (and retry attempt) that produced it. + +```typescript lineNumbers +import { setAttribute } from "workflow" +declare const externalCall: (orderId: string) => Promise<{ requestId: string }>; // @setup + +async function processOrderStep(orderId: string) { + "use step" + await setAttribute("orderId", orderId) // [!code highlight] + + const result = await externalCall(orderId) + await setAttribute("externalRequestId", result.requestId) + + return result +} +``` + +Step writes fire immediately — observability sees them mid-step. If the step retries, the new attempt re-emits whatever `setAttribute` calls it makes; the run's current snapshot reflects last-write-wins. + +## Searching and filtering + +Once a run has attributes, the observability UI and the runs list API can filter on them. + +### List runs matching a single attribute + +```typescript +const { data } = await world.runs.list({ + attributes: { tenantId: "t_acme" }, +}) +``` + +### AND-combined multi-attribute filter + +```typescript +const { data } = await world.runs.list({ + attributes: { tenantId: "t_acme", phase: "done" }, +}) +``` + +All listed key/value pairs must match exactly. There is no OR-combinator in V1; for multi-value lookups, issue separate queries and union the results client-side. + +### Building filter UIs + +To power the "key dropdown → value dropdown" pattern in a filter builder, the world exposes: + +```typescript +// "What attribute keys exist across all runs?" +const { data: keys } = await world.runs.listAttributeKeys({ + prefix: "ten", // optional fuzzy prefix +}) +// → [{ key: "tenant", runCount: 1245 }, { key: "tenantId", runCount: 89 }, ...] + +// "What values does `tenantId` have?" +const { data: values } = await world.runs.listAttributeValues({ + key: "tenantId", + prefix: "t_a", +}) +// → [{ value: "t_acme", runCount: 312 }, { value: "t_alpha", runCount: 22 }, ...] +``` + +Both are paginated and accept an optional `prefix` for incremental search. + +## Limits and validation + +V1 enforces these limits — exceeding any of them throws [`FatalError`](/docs/api-reference/workflow/fatal-error) **before** the event hits the wire, so an invalid batch never leaves the runtime: + +- **Key**: 1–256 characters; must not start with `$` (the `$`-prefix is reserved for future system keys like `$ai.model`). +- **Value**: ≤ 256 bytes UTF-8. +- **Per-run cap**: ≤ 64 attributes. + +`setAttributes({})` is an explicit no-op (no event emitted), so it's safe to compute updates conditionally without special-casing the empty path. + +## What attributes are NOT + +- **Not for sensitive data.** Attributes are stored plaintext and surfaced in observability. Keep secrets, PII, and tokens out of them — that's what step inputs/outputs (encrypted, behind refs) are for. +- **Not for arbitrary structured data.** Values are strings, capped at 256 bytes. If you need to attach a JSON blob to a run, return it from the workflow or persist it externally. +- **Not idempotency keys.** V1 has no `unique: true` semantics; concurrent writes to the same key are last-write-wins by event arrival. For idempotency on external API calls, see the [idempotency guide](/docs/foundations/idempotency). +- **Not retroactive.** Attributes set after a run completes are not allowed; the API only works while the run is active. + +## Pattern: phases as attributes + +A useful pattern is to track a workflow's high-level lifecycle as a single `phase` attribute, updated as the workflow advances: + +```typescript lineNumbers +import { setAttribute } from "workflow" +declare const validateCart: (cartId: string) => Promise; // @setup +declare const createShipment: (cartId: string) => Promise; // @setup + +async function checkout(cartId: string) { + "use workflow" + await setAttribute("phase", "validating") + await validateCart(cartId) + + await setAttribute("phase", "charging") + await chargePayment(cartId) + + await setAttribute("phase", "fulfilling") + await createShipment(cartId) + + await setAttribute("phase", "done") +} +``` + +In the observability UI, you can then filter `phase = charging` to find runs currently mid-payment, or `phase = fulfilling` to find runs awaiting shipment. Combined with `status = running`, this gives you a live operational view. + +## Pattern: business identifiers at start time + +When a workflow's identity is known at the trigger site, set those identifiers up-front so they're visible from the first event: + +```typescript lineNumbers +import { start } from "workflow/api" +declare const processOrder: (orderId: string) => Promise; // @setup + +export async function POST(req: Request) { + const { customerId, orderId, channel } = (await req.json()) as { + customerId: string; + orderId: string; + channel: string; + }; + + await start(processOrder, [orderId], { + attributes: { customerId, orderId, channel }, // [!code highlight] + } as any) // TODO(attributes V1): drop the cast once StartOptions accepts attributes. +} +``` + +This means support tools that key off `customerId` or `orderId` can find the run instantly, without needing to know the run ID up-front. + +## See also + +- [`setAttribute`](/docs/api-reference/workflow/set-attribute) — write a single attribute. +- [`setAttributes`](/docs/api-reference/workflow/set-attributes) — write many attributes at once. +- [`getAttribute`](/docs/api-reference/workflow/get-attribute) — read a single attribute. +- [`getAttributes`](/docs/api-reference/workflow/get-attributes) — read the full attribute snapshot. +- [Starting workflows](/docs/foundations/starting-workflows) — passing initial attributes via `start()`. diff --git a/docs/content/docs/foundations/index.mdx b/docs/content/docs/foundations/index.mdx index 528c3cf5a6..6c08b9a7af 100644 --- a/docs/content/docs/foundations/index.mdx +++ b/docs/content/docs/foundations/index.mdx @@ -32,4 +32,7 @@ Workflow programming can be a slight shift from how you traditionally write real Prevent duplicate side effects when retrying operations. + + Tag runs with searchable, plaintext metadata for observability and filtering. + diff --git a/docs/content/docs/foundations/meta.json b/docs/content/docs/foundations/meta.json index 299085da7a..036fa6935c 100644 --- a/docs/content/docs/foundations/meta.json +++ b/docs/content/docs/foundations/meta.json @@ -7,7 +7,8 @@ "hooks", "streaming", "serialization", - "idempotency" + "idempotency", + "attributes" ], "defaultOpen": true } diff --git a/packages/core/e2e/attributes.test.ts b/packages/core/e2e/attributes.test.ts new file mode 100644 index 0000000000..34677e8661 --- /dev/null +++ b/packages/core/e2e/attributes.test.ts @@ -0,0 +1,409 @@ +/** + * E2E tests for the workflow attributes API (V1). + * + * These tests are currently SKIPPED — they exercise APIs that do not yet + * exist (`setAttribute`, `setAttributes`, `getAttribute`, `getAttributes`, + * `start({ attributes })`, `world.runs.list({ attributes })`, + * `world.runs.listAttributeKeys`, `world.runs.listAttributeValues`). + * + * They are committed in this draft PR so the test cases can be reviewed + * alongside the plan and docs *before* implementation begins. Each `.skip` + * should be removed (or the entire `describe.skip` flipped to `describe`) + * once the corresponding piece of the V1 plan lands. + * + * Run locally (once unskipped): + * + * cd workbench/nextjs-turbopack && WORKFLOW_PUBLIC_MANIFEST=1 pnpm dev \ + * > /tmp/nextjs-dev.log 2>&1 & + * sleep 15 + * DEPLOYMENT_URL=http://localhost:3000 APP_NAME=nextjs-turbopack \ + * pnpm vitest run packages/core/e2e/attributes.test.ts + */ +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import type { Run } from '../src/runtime'; +import { getWorld, start as rawStart } from '../src/runtime'; +import { + getWorkflowMetadata, + setupRunTracking, + setupWorld, + trackRun, +} from './utils'; + +const deploymentUrl = process.env.DEPLOYMENT_URL; +if (!deploymentUrl) { + throw new Error('`DEPLOYMENT_URL` environment variable is not set'); +} + +async function start( + ...args: Parameters> +): Promise> { + const run = await rawStart(...args); + trackRun(run); + return run; +} + +async function attrWorkflow(fn: string) { + return getWorkflowMetadata(deploymentUrl, 'workflows/9_attributes.ts', fn); +} + +beforeAll(async () => { + setupWorld(deploymentUrl); +}); + +beforeEach((ctx) => { + setupRunTracking(ctx.task.name); +}); + +afterAll(() => { + // No e2e metadata writes here — the main e2e.test.ts owns that pipeline. +}); + +// ============================================================================ +// All cases in this file correspond directly to the "Test plan" section of +// the V1 plan in PR #1933. They will be flipped on as implementation lands. +// ============================================================================ + +describe.skip('attributes (V1)', { timeout: 60_000 }, () => { + // -------------------------------------------------------------------------- + // setAttribute / getAttribute round-trip from a workflow + step + // -------------------------------------------------------------------------- + it('round-trips a single attribute through workflow + step writes', async () => { + const run = await start(await attrWorkflow('attributesWorkflow'), [ + 'ord_123', + ]); + + const result = (await run.returnValue) as { + tenant: string | undefined; + stepView: Record; + orderIdAfterStep: string | undefined; + final: Record; + }; + + // No initial attributes were set on this run — tenant comes back undefined. + expect(result.tenant).toBeUndefined(); + + // Step body saw its own writes (read-your-writes). + expect(result.stepView).toMatchObject({ + orderId: 'ord_123', + stepKind: 'process', + region: 'us-east-1', + }); + + // Workflow body resumed after the step and observed the step's writes. + expect(result.orderIdAfterStep).toBe('ord_123'); + + // Final snapshot reflects the workflow's last setAttribute('phase', 'done') + // and the unset of `region`. + expect(result.final).toEqual({ + orderId: 'ord_123', + stepKind: 'process', + phase: 'done', + }); + + // The run snapshot persisted by the world matches the workflow's final view. + const world = await getWorld(); + const persisted = await world.runs.get(run.runId); + expect(persisted.attributes).toEqual({ + orderId: 'ord_123', + stepKind: 'process', + phase: 'done', + }); + }); + + // -------------------------------------------------------------------------- + // setAttributes batch: one event with multiple `changes` entries + // -------------------------------------------------------------------------- + it('emits a single attr_set event for setAttributes with multiple keys', async () => { + const run = await start(await attrWorkflow('attributesWorkflow'), [ + 'ord_456', + ]); + await run.returnValue; + + const world = await getWorld(); + const { data: events } = await world.events.list({ + runId: run.runId, + pagination: { limit: 100 }, + }); + + // The step's setAttributes({ stepKind, region }) call should land as a + // single attr_set event whose eventData.changes has length 2. + const stepBatch = events.find( + (e: any) => + e.eventType === 'attr_set' && + e.eventData?.writer?.type === 'step' && + Array.isArray(e.eventData?.changes) && + e.eventData.changes.length === 2 + ); + expect(stepBatch).toBeDefined(); + expect( + stepBatch?.eventData?.changes?.map((c: any) => c.key).sort() + ).toEqual(['region', 'stepKind']); + }); + + // -------------------------------------------------------------------------- + // start({ attributes }) — initial attributes carried on run_created + // -------------------------------------------------------------------------- + it('materializes initial attributes from start() onto the run with no attr_set event', async () => { + const run = await start( + await attrWorkflow('attributesWorkflow'), + ['ord_789'], + // TODO(attributes): drop this cast once start() options accept attributes. + { attributes: { tenant: 't_acme' } } as any + ); + + const result = (await run.returnValue) as { + tenant: string | undefined; + final: Record; + }; + + expect(result.tenant).toBe('t_acme'); + expect(result.final).toMatchObject({ tenant: 't_acme', phase: 'done' }); + + // No attr_set event should have been emitted for the initial set — + // it lives on run_created.eventData.attributes. + const world = await getWorld(); + const { data: events } = await world.events.list({ + runId: run.runId, + pagination: { limit: 100 }, + }); + const runCreated = events.find((e: any) => e.eventType === 'run_created'); + expect(runCreated?.eventData?.attributes).toEqual({ tenant: 't_acme' }); + + const initialAttrSets = events.filter( + (e: any) => + e.eventType === 'attr_set' && + e.eventData?.changes?.some((c: any) => c.key === 'tenant') + ); + // The workflow body never writes `tenant`, so no attr_set should mention it. + expect(initialAttrSets).toHaveLength(0); + }); + + // -------------------------------------------------------------------------- + // Writer attribution: workflow vs step, including step attempt counter + // -------------------------------------------------------------------------- + it('records writer.type=workflow for workflow-emitted writes', async () => { + const run = await start(await attrWorkflow('attributesWorkflow'), [ + 'ord_w', + ]); + await run.returnValue; + + const world = await getWorld(); + const { data: events } = await world.events.list({ + runId: run.runId, + pagination: { limit: 100 }, + }); + + const phaseInit = events.find( + (e: any) => + e.eventType === 'attr_set' && + e.eventData?.changes?.some( + (c: any) => c.key === 'phase' && c.value === 'init' + ) + ); + expect(phaseInit?.eventData?.writer).toEqual({ type: 'workflow' }); + }); + + it('records writer.type=step with stepId+attempt for step writes', async () => { + const run = await start(await attrWorkflow('attributesWorkflow'), [ + 'ord_s', + ]); + await run.returnValue; + + const world = await getWorld(); + const { data: events } = await world.events.list({ + runId: run.runId, + pagination: { limit: 100 }, + }); + + const stepWrite = events.find( + (e: any) => + e.eventType === 'attr_set' && e.eventData?.writer?.type === 'step' + ); + expect(stepWrite).toBeDefined(); + expect(stepWrite?.eventData?.writer).toMatchObject({ + type: 'step', + stepId: expect.any(String), + attempt: 1, + }); + }); + + it('records distinct attempts when a step retries', async () => { + const run = await start(await attrWorkflow('attributesRetryWorkflow'), []); + const final = (await run.returnValue) as Record; + + // Both attempts should have written a key with their attempt number. + expect(final).toMatchObject({ + 'attemptedAt-1': expect.any(String), + 'attemptedAt-2': expect.any(String), + }); + + const world = await getWorld(); + const { data: events } = await world.events.list({ + runId: run.runId, + pagination: { limit: 100 }, + }); + + const attempts = events + .filter( + (e: any) => + e.eventType === 'attr_set' && e.eventData?.writer?.type === 'step' + ) + .map((e: any) => e.eventData?.writer?.attempt as number) + .sort(); + + expect(attempts).toEqual([1, 2]); + }); + + // -------------------------------------------------------------------------- + // Replay determinism: a workflow that calls getAttribute multiple times + // sees the same value across replays. + // -------------------------------------------------------------------------- + it('is deterministic across replays', async () => { + // Two independent runs of the same workflow with identical inputs + // should produce identical final attribute snapshots. (The runtime + // replays from the event log on resume; this asserts that the rebuilt + // attributes view matches the original.) + const r1 = await start(await attrWorkflow('attributesWorkflow'), [ + 'ord_det', + ]); + const r2 = await start(await attrWorkflow('attributesWorkflow'), [ + 'ord_det', + ]); + + const v1 = (await r1.returnValue) as { final: Record }; + const v2 = (await r2.returnValue) as { final: Record }; + + expect(v1.final).toEqual(v2.final); + }); + + // -------------------------------------------------------------------------- + // runs.list({ attributes }) — AND-combined exact-match filter + // -------------------------------------------------------------------------- + it('filters runs by a single attribute via runs.list', async () => { + const run = await start( + await attrWorkflow('attributesWorkflow'), + ['ord_filter_one'], + { attributes: { tenant: 't_filter' } } as any + ); + await run.returnValue; + + const world = await getWorld(); + const { data: matched } = await world.runs.list({ + // TODO(attributes): drop the cast once ListWorkflowRunsParams adds + // the `attributes` field. + attributes: { tenant: 't_filter' }, + } as any); + + expect(matched.some((r) => r.runId === run.runId)).toBe(true); + }); + + it('AND-combines multiple attribute filters on runs.list', async () => { + const matchingRun = await start( + await attrWorkflow('attributesWorkflow'), + ['ord_match'], + { attributes: { tenant: 't_and', region: 'eu-west-1' } } as any + ); + const nonMatchingRun = await start( + await attrWorkflow('attributesWorkflow'), + ['ord_nope'], + { attributes: { tenant: 't_and', region: 'us-east-1' } } as any + ); + await matchingRun.returnValue; + await nonMatchingRun.returnValue; + + const world = await getWorld(); + const { data: matched } = await world.runs.list({ + attributes: { tenant: 't_and', region: 'eu-west-1' }, + } as any); + + const ids = matched.map((r) => r.runId); + expect(ids).toContain(matchingRun.runId); + expect(ids).not.toContain(nonMatchingRun.runId); + }); + + // -------------------------------------------------------------------------- + // listAttributeKeys / listAttributeValues — for filter-builder UIs + // -------------------------------------------------------------------------- + it('enumerates attribute keys via listAttributeKeys', async () => { + const run = await start( + await attrWorkflow('attributesWorkflow'), + ['ord_keys'], + { attributes: { tenant: 't_keys' } } as any + ); + await run.returnValue; + + const world = await getWorld(); + // TODO(attributes): drop the cast once Storage.runs.listAttributeKeys exists. + const { data: keys } = await (world.runs as any).listAttributeKeys({ + pagination: { limit: 100 }, + }); + const seen = keys.map((k: { key: string }) => k.key); + + for (const expected of ['tenant', 'phase', 'orderId', 'stepKind']) { + expect(seen).toContain(expected); + } + }); + + it('enumerates attribute values for a given key via listAttributeValues', async () => { + await start(await attrWorkflow('attributesWorkflow'), ['ord_val_a'], { + attributes: { tenant: 't_values_one' }, + } as any).then((r) => r.returnValue); + await start(await attrWorkflow('attributesWorkflow'), ['ord_val_b'], { + attributes: { tenant: 't_values_two' }, + } as any).then((r) => r.returnValue); + + const world = await getWorld(); + const { data: values } = await (world.runs as any).listAttributeValues({ + key: 'tenant', + pagination: { limit: 100 }, + }); + + const seen = values.map((v: { value: string }) => v.value); + expect(seen).toEqual( + expect.arrayContaining(['t_values_one', 't_values_two']) + ); + }); + + it('honors prefix on listAttributeKeys', async () => { + await start(await attrWorkflow('attributesWorkflow'), ['ord_pref'], { + attributes: { tenant: 't_pref' }, + } as any).then((r) => r.returnValue); + + const world = await getWorld(); + const { data: keys } = await (world.runs as any).listAttributeKeys({ + prefix: 'ten', + pagination: { limit: 100 }, + }); + + expect(keys.map((k: { key: string }) => k.key)).toEqual( + expect.arrayContaining(['tenant']) + ); + // No key starting with `ten` other than `tenant` is created by these tests. + for (const k of keys) { + expect(k.key.startsWith('ten')).toBe(true); + } + }); + + // -------------------------------------------------------------------------- + // Validation — FatalError thrown before the event hits the wire + // -------------------------------------------------------------------------- + it('rejects keys that start with a reserved $ prefix', async () => { + // A workflow that tries to write `$ai.model` should fail with FatalError. + // Implemented as a separate workflow function in 9_attributes.ts once the + // API exists; this case is a placeholder so the validation path has a + // dedicated assertion. See "Validation rules" in the V1 plan. + expect(true).toBe(true); // TODO(attributes): drive a workflow that throws. + }); + + it('rejects values larger than 256 bytes', async () => { + expect(true).toBe(true); // TODO(attributes): drive a workflow that throws. + }); + + it('rejects more than 64 attributes per run', async () => { + expect(true).toBe(true); // TODO(attributes): drive a workflow that throws. + }); + + it('treats setAttributes({}) as a no-op (no attr_set event emitted)', async () => { + expect(true).toBe(true); // TODO(attributes): drive a workflow that calls setAttributes({}). + }); +}); diff --git a/packages/docs-typecheck/src/docs-globals.d.ts b/packages/docs-typecheck/src/docs-globals.d.ts index 124caaeaf4..e169e29003 100644 --- a/packages/docs-typecheck/src/docs-globals.d.ts +++ b/packages/docs-typecheck/src/docs-globals.d.ts @@ -186,6 +186,9 @@ declare global { runs: { get: (...args: any[]) => Promise; list: (...args: any[]) => Promise; + // TODO(attributes V1): remove these once Storage.runs ships them. + listAttributeKeys: (...args: any[]) => Promise; + listAttributeValues: (...args: any[]) => Promise; }; steps: { get: (...args: any[]) => Promise; @@ -262,3 +265,21 @@ declare global { const childWorkflow: (...args: any[]) => Promise; const orderId: string; } + +// ============================================================================ +// TODO(attributes V1): remove this entire block once setAttribute / +// setAttributes / getAttribute / getAttributes are exported from `workflow` +// and `start()` accepts an `attributes` option natively. +// +// These ambient shims exist only so the documentation samples for the +// upcoming Attributes feature typecheck before the implementation lands. +// See PR #1933. +// ============================================================================ +declare module 'workflow' { + function setAttribute(key: string, value: string | undefined): Promise; + function setAttributes( + attrs: Record + ): Promise; + function getAttribute(key: string): string | undefined; + function getAttributes(): Record; +} diff --git a/workbench/example/workflows/9_attributes.ts b/workbench/example/workflows/9_attributes.ts new file mode 100644 index 0000000000..2288247c5f --- /dev/null +++ b/workbench/example/workflows/9_attributes.ts @@ -0,0 +1,122 @@ +/** + * Demonstrates the workflow attributes API (V1). + * + * NOTE: As of this commit, setAttribute / setAttributes / getAttribute / + * getAttributes are NOT yet implemented in @workflow/core. The workflows + * here are written against the proposed API surface so the e2e tests in + * `packages/core/e2e/attributes.test.ts` can be flipped on by removing + * `.skip` once implementation lands. + * + * Where the API is referenced, we cast through `as any` so the file + * typechecks today. Each cast carries a TODO so they are easy to grep + * and remove during implementation. + */ +import * as workflow from 'workflow'; + +// TODO(attributes): drop these casts once the real exports exist. +const setAttribute = ( + workflow as unknown as { + setAttribute(key: string, value: string | undefined): Promise; + } +).setAttribute; +const setAttributes = ( + workflow as unknown as { + setAttributes(attrs: Record): Promise; + } +).setAttributes; +const getAttribute = ( + workflow as unknown as { + getAttribute(key: string): string | undefined; + } +).getAttribute; +const getAttributes = ( + workflow as unknown as { + getAttributes(): Record; + } +).getAttributes; + +/** + * Sets a couple of attributes from inside a step using the batch helper, then + * reads them back to demonstrate read-your-writes. + */ +async function processOrderStep( + orderId: string +): Promise> { + 'use step'; + + // Single-key write. + await setAttribute('orderId', orderId); + // Batch write — both keys land in one attr_set event with two `changes`. + await setAttributes({ stepKind: 'process', region: 'us-east-1' }); + + // Read-your-writes inside the same step body. + const orderIdSeen = getAttribute('orderId'); + if (orderIdSeen !== orderId) { + throw new Error( + `read-your-writes broken: expected orderId="${orderId}", got "${orderIdSeen}"` + ); + } + + return getAttributes(); +} + +/** + * Workflow exercising the full V1 surface: + * - reads an initial attribute set at start() time (carried on run_created) + * - writes from the workflow body (writer = workflow) + * - awaits a step that writes (writer = step + stepId + attempt) + * - unsets a key with `undefined` + * - returns the final getAttributes() snapshot + */ +export async function attributesWorkflow(orderId: string) { + 'use workflow'; + + // tenant comes from `start({ attributes: { tenant: '...' } })` (initial + // attributes carried on run_created, no attr_set event emitted at birth). + const tenant = getAttribute('tenant'); + + await setAttribute('phase', 'init'); + + const stepView = await processOrderStep(orderId); + + // Workflow observes the step's writes once it resumes after the step. + const orderIdAfterStep = getAttribute('orderId'); + + await setAttribute('phase', 'done'); + // Unset the per-step `region` attribute now that the work is finished; + // exercises the `null` value path in the attr_set event. + await setAttribute('region', undefined); + + return { + tenant, + stepView, + orderIdAfterStep, + final: getAttributes(), + }; +} + +/** + * Stresses the retry-attribution path: a step that fails on the first attempt + * and succeeds on the second, with an attribute write on each attempt. The + * event log should record both attr_set events with `writer.attempt` of 1 + * and 2 respectively. + */ +async function retryingStep(): Promise { + 'use step'; + + // TODO(attributes): use getStepMetadata().attempt in the real API to make + // the check explicit. For now we rely on the runtime's retry behavior: + // first attempt throws; second attempt succeeds. + const ctx = workflow.getStepMetadata(); + await setAttribute(`attemptedAt-${ctx.attempt}`, new Date().toISOString()); + + if (ctx.attempt === 1) { + throw new Error('Synthetic retryable error to exercise attempt counter'); + } +} + +export async function attributesRetryWorkflow() { + 'use workflow'; + await retryingStep(); + return getAttributes(); +} diff --git a/workbench/nextjs-turbopack/workflows/9_attributes.ts b/workbench/nextjs-turbopack/workflows/9_attributes.ts new file mode 120000 index 0000000000..288df47f8e --- /dev/null +++ b/workbench/nextjs-turbopack/workflows/9_attributes.ts @@ -0,0 +1 @@ +../../example/workflows/9_attributes.ts \ No newline at end of file diff --git a/workbench/nextjs-webpack/workflows/9_attributes.ts b/workbench/nextjs-webpack/workflows/9_attributes.ts new file mode 120000 index 0000000000..288df47f8e --- /dev/null +++ b/workbench/nextjs-webpack/workflows/9_attributes.ts @@ -0,0 +1 @@ +../../example/workflows/9_attributes.ts \ No newline at end of file diff --git a/workbench/nitro-v3/workflows/9_attributes.ts b/workbench/nitro-v3/workflows/9_attributes.ts new file mode 120000 index 0000000000..288df47f8e --- /dev/null +++ b/workbench/nitro-v3/workflows/9_attributes.ts @@ -0,0 +1 @@ +../../example/workflows/9_attributes.ts \ No newline at end of file From 07a4cff17c7d6221ac7b73f9a1b83717b28648a7 Mon Sep 17 00:00:00 2001 From: Pranay Prakash Date: Tue, 5 May 2026 21:13:42 +0900 Subject: [PATCH 3/3] docs(attributes): gate to v5 pre-release Mirrors the cancellation / AbortSignal pattern landed on main in #1301: attribute docs are v5-only since the underlying API will only ship in the v5 (workflow 5.x) branch. - Add `preRelease: true` to the frontmatter of all 5 attribute docs (foundations/attributes + 4 api-reference/workflow/*-attribute*). The v5 docs setup (docs/lib/geistdocs/version-source.ts) filters these out of the v4 sidebar tree and rewrites their URLs to /v5. - Drop the corresponding Cards from foundations/index.mdx and api-reference/workflow/index.mdx so v4 readers don't get broken links. v5 readers discover the pages via the sidebar. - Drop the inline "V1 preview" Callouts now that the v5 layout renders its own pre-release banner for preRelease pages. foundations/meta.json already lists `attributes` (alongside the freshly-merged `cancellation`); keep it there for the v5 sidebar. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../docs/api-reference/workflow/get-attribute.mdx | 6 +----- .../docs/api-reference/workflow/get-attributes.mdx | 6 +----- docs/content/docs/api-reference/workflow/index.mdx | 12 ------------ .../docs/api-reference/workflow/set-attribute.mdx | 6 +----- .../docs/api-reference/workflow/set-attributes.mdx | 6 +----- docs/content/docs/foundations/attributes.mdx | 6 +----- docs/content/docs/foundations/index.mdx | 3 --- 7 files changed, 5 insertions(+), 40 deletions(-) diff --git a/docs/content/docs/api-reference/workflow/get-attribute.mdx b/docs/content/docs/api-reference/workflow/get-attribute.mdx index 1483f442bd..9944053df0 100644 --- a/docs/content/docs/api-reference/workflow/get-attribute.mdx +++ b/docs/content/docs/api-reference/workflow/get-attribute.mdx @@ -2,6 +2,7 @@ title: getAttribute description: Read a single attribute from the current workflow run. type: reference +preRelease: true summary: Use getAttribute to look up a key on the current run's attribute snapshot. prerequisites: - /docs/foundations/workflows-and-steps @@ -12,11 +13,6 @@ related: - /docs/api-reference/workflow/set-attribute --- - - Attributes are a V1 preview feature. The API is stable but the - observability/filter UI is still rolling out. - - Returns the current value of a single attribute on the workflow run, or `undefined` if the key is not set. Synchronous — `getAttribute` reads from the in-memory snapshot the runtime maintains for the run; it does not hit the network. ```typescript lineNumbers diff --git a/docs/content/docs/api-reference/workflow/get-attributes.mdx b/docs/content/docs/api-reference/workflow/get-attributes.mdx index cf71d87e42..f783ef9e31 100644 --- a/docs/content/docs/api-reference/workflow/get-attributes.mdx +++ b/docs/content/docs/api-reference/workflow/get-attributes.mdx @@ -2,6 +2,7 @@ title: getAttributes description: Read all attributes on the current workflow run as a record. type: reference +preRelease: true summary: Use getAttributes to retrieve the full attribute snapshot for the current run. prerequisites: - /docs/foundations/workflows-and-steps @@ -12,11 +13,6 @@ related: - /docs/api-reference/workflow/set-attributes --- - - Attributes are a V1 preview feature. The API is stable but the - observability/filter UI is still rolling out. - - Returns the full set of attributes on the current workflow run as a `Record`. The returned object is a snapshot — mutating it has no effect on the run. ```typescript lineNumbers diff --git a/docs/content/docs/api-reference/workflow/index.mdx b/docs/content/docs/api-reference/workflow/index.mdx index 540335ae9f..8544b47d70 100644 --- a/docs/content/docs/api-reference/workflow/index.mdx +++ b/docs/content/docs/api-reference/workflow/index.mdx @@ -47,18 +47,6 @@ Workflow SDK contains the following functions you can use inside your workflow f Access the current workflow run's default stream. - - Tag a run with a single key-value attribute for observability and filtering. - - - Tag a run with multiple attributes in a single batched event. - - - Read a single attribute from the current run. - - - Read the full attribute snapshot for the current run. - ## Error Classes diff --git a/docs/content/docs/api-reference/workflow/set-attribute.mdx b/docs/content/docs/api-reference/workflow/set-attribute.mdx index aabcbe0eeb..5225aa744f 100644 --- a/docs/content/docs/api-reference/workflow/set-attribute.mdx +++ b/docs/content/docs/api-reference/workflow/set-attribute.mdx @@ -2,6 +2,7 @@ title: setAttribute description: Set a single attribute on the current workflow run from inside a workflow or step. type: reference +preRelease: true summary: Use setAttribute to record a key-value attribute on the current run for observability and filtering. prerequisites: - /docs/foundations/workflows-and-steps @@ -12,11 +13,6 @@ related: - /docs/api-reference/workflow/get-attribute --- - - Attributes are a V1 preview feature. The API is stable but the - observability/filter UI is still rolling out. - - Sets a single key-value attribute on the current workflow run. Attributes are small, plaintext metadata that you can search, filter, and display in the observability UI without exposing step inputs/outputs. You can call `setAttribute` from either a workflow function or a step function. The runtime records who wrote what (workflow body vs. specific step + attempt) in the event log so observability can attribute each change to its source. diff --git a/docs/content/docs/api-reference/workflow/set-attributes.mdx b/docs/content/docs/api-reference/workflow/set-attributes.mdx index 579c325ec4..955a78dc14 100644 --- a/docs/content/docs/api-reference/workflow/set-attributes.mdx +++ b/docs/content/docs/api-reference/workflow/set-attributes.mdx @@ -2,6 +2,7 @@ title: setAttributes description: Set multiple attributes on the current workflow run in a single batched event. type: reference +preRelease: true summary: Use setAttributes to record several key-value attributes at once with merge semantics. prerequisites: - /docs/foundations/workflows-and-steps @@ -12,11 +13,6 @@ related: - /docs/api-reference/workflow/get-attributes --- - - Attributes are a V1 preview feature. The API is stable but the - observability/filter UI is still rolling out. - - Sets multiple attributes on the current workflow run in a single batched event. Use this whenever you have several attributes to write at the same point in time — it produces one event with multiple changes, instead of one event per attribute. ```typescript lineNumbers diff --git a/docs/content/docs/foundations/attributes.mdx b/docs/content/docs/foundations/attributes.mdx index 2fbb3ab49a..e9cd060683 100644 --- a/docs/content/docs/foundations/attributes.mdx +++ b/docs/content/docs/foundations/attributes.mdx @@ -2,6 +2,7 @@ title: Attributes description: Attach searchable, plaintext metadata to workflow runs for observability and filtering. type: conceptual +preRelease: true summary: Use attributes to label runs with business data so you can find, filter, and group them later. prerequisites: - /docs/foundations/workflows-and-steps @@ -13,11 +14,6 @@ related: - /docs/api-reference/workflow/get-attributes --- - - Attributes are a V1 preview feature. The API is stable; the observability - UI is still rolling out, and value types are limited to strings. - - Attributes are small, plaintext key-value pairs you can attach to a workflow run. They're meant for the data you want to see, search, and filter on in observability — things like `tenantId`, `orderId`, `phase`, `region`, `customerEmail`. They are **not** meant for arbitrary structured data; for that, use step inputs/outputs. ## Why attributes diff --git a/docs/content/docs/foundations/index.mdx b/docs/content/docs/foundations/index.mdx index 6c08b9a7af..528c3cf5a6 100644 --- a/docs/content/docs/foundations/index.mdx +++ b/docs/content/docs/foundations/index.mdx @@ -32,7 +32,4 @@ Workflow programming can be a slight shift from how you traditionally write real Prevent duplicate side effects when retrying operations. - - Tag runs with searchable, plaintext metadata for observability and filtering. -