From 3f0e2ef4ca19aeefdf935e7d6e4a19ff7947dfc0 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 10 Mar 2026 14:26:34 -0700 Subject: [PATCH 1/6] Add documentation for custom class serialization --- .changeset/smooth-nails-do.md | 2 + docs/content/docs/api-reference/index.mdx | 3 + docs/content/docs/api-reference/meta.json | 2 +- .../api-reference/workflow-serde/index.mdx | 52 ++++++++ .../api-reference/workflow-serde/meta.json | 3 + .../workflow-serde/workflow-deserialize.mdx | 68 ++++++++++ .../workflow-serde/workflow-serialize.mdx | 73 +++++++++++ .../docs/foundations/serialization.mdx | 122 ++++++++++++++++++ packages/docs-typecheck/src/extractor.ts | 8 +- 9 files changed, 330 insertions(+), 3 deletions(-) create mode 100644 .changeset/smooth-nails-do.md create mode 100644 docs/content/docs/api-reference/workflow-serde/index.mdx create mode 100644 docs/content/docs/api-reference/workflow-serde/meta.json create mode 100644 docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx create mode 100644 docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx diff --git a/.changeset/smooth-nails-do.md b/.changeset/smooth-nails-do.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/smooth-nails-do.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/docs/content/docs/api-reference/index.mdx b/docs/content/docs/api-reference/index.mdx index e2bdfb8e0e..82105dc375 100644 --- a/docs/content/docs/api-reference/index.mdx +++ b/docs/content/docs/api-reference/index.mdx @@ -17,6 +17,9 @@ All the functions and primitives that come with Workflow DevKit by package. Next.js integration for Workflow DevKit that automatically configures bundling and runtime support. + + Serialization symbols for custom class serialization in workflows. + Helpers for integrating AI SDK for building AI-powered workflows. diff --git a/docs/content/docs/api-reference/meta.json b/docs/content/docs/api-reference/meta.json index d046d7b20a..481a06207b 100644 --- a/docs/content/docs/api-reference/meta.json +++ b/docs/content/docs/api-reference/meta.json @@ -1,4 +1,4 @@ { "title": "API Reference", - "pages": ["...", "workflow-ai", "vitest"] + "pages": ["...", "workflow-serde", "workflow-ai", "vitest"] } diff --git a/docs/content/docs/api-reference/workflow-serde/index.mdx b/docs/content/docs/api-reference/workflow-serde/index.mdx new file mode 100644 index 0000000000..80b377207f --- /dev/null +++ b/docs/content/docs/api-reference/workflow-serde/index.mdx @@ -0,0 +1,52 @@ +--- +title: "@workflow/serde" +--- + +Serialization symbols for custom class serialization in Workflow DevKit. + +## Installation + +```package-install +npm i @workflow/serde +``` + +## Overview + +By default, Workflow DevKit can serialize standard JavaScript types like primitives, objects, arrays, `Date`, `Map`, `Set`, and more. However, custom class instances are not serializable by default because the serialization system doesn't know how to reconstruct them. + +The `@workflow/serde` package provides two symbols that allow you to define custom serialization and deserialization logic for your classes, enabling them to be passed between workflow and step functions. + +## Symbols + + + + Symbol for defining how to serialize a class instance to plain data. + + + Symbol for defining how to reconstruct a class instance from plain data. + + + +## Quick Example + +{/* @expect-error:2351 */} + +```typescript lineNumbers +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; + +class Point { + constructor(public x: number, public y: number) {} + + static [WORKFLOW_SERIALIZE](instance: Point) { + return { x: instance.x, y: instance.y }; + } + + static [WORKFLOW_DESERIALIZE](data: { x: number; y: number }) { + return new Point(data.x, data.y); + } +} +``` + + +For a complete guide on custom class serialization, see the [Serialization documentation](/docs/foundations/serialization#custom-class-serialization). + diff --git a/docs/content/docs/api-reference/workflow-serde/meta.json b/docs/content/docs/api-reference/workflow-serde/meta.json new file mode 100644 index 0000000000..856b0fc93e --- /dev/null +++ b/docs/content/docs/api-reference/workflow-serde/meta.json @@ -0,0 +1,3 @@ +{ + "pages": ["...", "workflow-serialize", "workflow-deserialize"] +} diff --git a/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx b/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx new file mode 100644 index 0000000000..2abcbe2922 --- /dev/null +++ b/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx @@ -0,0 +1,68 @@ +--- +title: WORKFLOW_DESERIALIZE +--- + +A symbol used to define custom deserialization for user-defined class instances. The static method should accept serialized data and return a new class instance. + +## Usage + +{/* @expect-error:2351 */} + +```typescript lineNumbers +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; + +class Point { + constructor(public x: number, public y: number) {} + + static [WORKFLOW_SERIALIZE](instance: Point) { + return { x: instance.x, y: instance.y }; + } + + static [WORKFLOW_DESERIALIZE](data: { x: number; y: number }) { + return new Point(data.x, data.y); + } +} +``` + +## API Signature + +```typescript +static [WORKFLOW_DESERIALIZE](data: SerializableData): T +``` + +### Parameters + + + +### Returns + +The method should return a new instance of the class, reconstructed from the serialized data. + +## Requirements + + +The method must be implemented as a **static** method on the class. Instance methods are not supported. + + +- Both `WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE` must be implemented together +- The method receives the exact data that was returned by `WORKFLOW_SERIALIZE` +- If `WORKFLOW_SERIALIZE` returns complex types (like `Map` or `Date`), they will be properly deserialized before being passed to this method + + +This method runs inside the workflow context and is subject to the same constraints as `"use workflow"` functions: +- No Node.js-specific APIs (like `fs`, `path`, `crypto`, etc.) +- No non-deterministic operations (like `Math.random()` or `Date.now()`) +- No external network calls + +Keep this method simple and focused on reconstructing the instance from the provided data. + diff --git a/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx b/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx new file mode 100644 index 0000000000..e7e4c6f10b --- /dev/null +++ b/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx @@ -0,0 +1,73 @@ +--- +title: WORKFLOW_SERIALIZE +--- + +A symbol used to define custom serialization for user-defined class instances. The static method should accept an instance and return serializable data. + +## Usage + +{/* @expect-error:2351 */} + +```typescript lineNumbers +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; + +class Point { + constructor(public x: number, public y: number) {} + + static [WORKFLOW_SERIALIZE](instance: Point) { + return { x: instance.x, y: instance.y }; + } + + static [WORKFLOW_DESERIALIZE](data: { x: number; y: number }) { + return new Point(data.x, data.y); + } +} +``` + +## API Signature + +```typescript +static [WORKFLOW_SERIALIZE](instance: T): SerializableData +``` + +### Parameters + + + +### Returns + +The method should return serializable data. This can be: + +- Primitives (`string`, `number`, `boolean`, `null`, `undefined`, `bigint`) +- Plain objects with serializable values +- Arrays of serializable values +- Built-in serializable types (`Date`, `Map`, `Set`, `RegExp`, `URL`, etc.) +- Other custom classes that implement serialization + +## Requirements + + +The method must be implemented as a **static** method on the class. Instance methods are not supported. + + +- Both `WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE` must be implemented together +- The returned data must itself be serializable +- The SWC compiler plugin automatically detects and registers classes that implement these symbols + + +This method runs inside the workflow context and is subject to the same constraints as `"use workflow"` functions: +- No Node.js-specific APIs (like `fs`, `path`, `crypto`, etc.) +- No non-deterministic operations (like `Math.random()` or `Date.now()`) +- No external network calls + +Keep this method simple and focused on extracting data from the instance. + diff --git a/docs/content/docs/foundations/serialization.mdx b/docs/content/docs/foundations/serialization.mdx index d94ee6ea73..5e3459f998 100644 --- a/docs/content/docs/foundations/serialization.mdx +++ b/docs/content/docs/foundations/serialization.mdx @@ -121,6 +121,128 @@ export async function fetch(...args: Parameters) { This allows you to make HTTP requests directly in workflow functions while maintaining deterministic replay behavior through automatic caching. +## Custom Class Serialization + +By default, custom class instances cannot be serialized because the serialization system doesn't know how to reconstruct them. You can make your classes serializable by implementing two static methods using special symbols from the `@workflow/serde` package. + +### Basic Example + +{/* @expect-error:2351 */} + +```typescript title="workflows/custom-class.ts" lineNumbers +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; + +class Point { + constructor( + public x: number, + public y: number + ) {} + + // Define how to serialize an instance to plain data + static [WORKFLOW_SERIALIZE](instance: Point) { + return { x: instance.x, y: instance.y }; + } + + // Define how to reconstruct an instance from plain data + static [WORKFLOW_DESERIALIZE](data: { x: number; y: number }) { + return new Point(data.x, data.y); + } +} +``` + +Once you've implemented these methods, instances of your class can be passed between workflow and step functions: + +{/* @expect-error:2351 */} + +```typescript title="workflows/geometry.ts" lineNumbers +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; + +class Point { + constructor(public x: number, public y: number) {} + static [WORKFLOW_SERIALIZE](instance: Point) { + return { x: instance.x, y: instance.y }; + } + static [WORKFLOW_DESERIALIZE](data: { x: number; y: number }) { + return new Point(data.x, data.y); + } +} + +export async function geometryWorkflow() { + "use workflow"; + + const point = new Point(10, 20); + const doubled = await doublePoint(point); // Point is serialized automatically + + console.log(doubled.x, doubled.y); // 20, 40 + return doubled; +} + +async function doublePoint(point: Point) { + "use step"; + return new Point(point.x * 2, point.y * 2); // Returns a new Point instance +} +``` + +### How It Works + +1. **`WORKFLOW_SERIALIZE`**: A static method that receives a class instance and returns serializable data (primitives, plain objects, arrays, etc.) + +2. **`WORKFLOW_DESERIALIZE`**: A static method that receives the serialized data and returns a new class instance + +3. **Automatic Registration**: The SWC compiler plugin automatically detects classes that implement these symbols and registers them for serialization + +### Requirements + + +Both methods must be implemented as **static** methods on the class. Instance methods are not supported. + + +- The data returned by `WORKFLOW_SERIALIZE` must itself be serializable (primitives, plain objects, arrays, or other serializable types including other custom classes) +- Both symbols must be implemented together - a class with only one will not be serializable + + +The `WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE` methods run inside the workflow context and are subject to the same constraints as `"use workflow"` functions. This means: +- No Node.js-specific APIs (like `fs`, `path`, `crypto`, etc.) +- No non-deterministic operations (like `Math.random()` or `Date.now()`) +- No external network calls + +Keep these methods simple and focused on data transformation only. + + +### Complex Example + +Custom serialization works with nested objects and other serializable types: + +{/* @expect-error:2351 */} + +```typescript title="workflows/complex-class.ts" lineNumbers +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; + +class Order { + constructor( + public id: string, + public items: Map, + public createdAt: Date + ) {} + + static [WORKFLOW_SERIALIZE](instance: Order) { + return { + id: instance.id, + items: instance.items, // Map is serializable + createdAt: instance.createdAt, // Date is serializable + }; + } + + static [WORKFLOW_DESERIALIZE](data: { + id: string; + items: Map; + createdAt: Date; + }) { + return new Order(data.id, data.items, data.createdAt); + } +} +``` + ## Pass-by-Value Semantics **Parameters are passed by value, not by reference.** Steps receive deserialized copies of data. Mutations inside a step won't affect the original in the workflow. diff --git a/packages/docs-typecheck/src/extractor.ts b/packages/docs-typecheck/src/extractor.ts index 2be7b306a3..729b9f69c7 100644 --- a/packages/docs-typecheck/src/extractor.ts +++ b/packages/docs-typecheck/src/extractor.ts @@ -2,7 +2,9 @@ import type { CodeSample } from './types.js'; const CODE_BLOCK_REGEX = /```(typescript|ts|javascript|js)(?:[^\S\n]+[^\n]*)?\n([\s\S]*?)```/g; -const EXPECT_ERROR_REGEX = //; +// Support both HTML comments () and MDX comments ({/* @expect-error:2351 */}) +const EXPECT_ERROR_REGEX = + /(?:|\{\/\*\s*@expect-error:([0-9,\s]+)\s*\*\/\})/; // Match entire line comments with [!code ...] including any trailing text const HIGHLIGHT_COMMENT_REGEX = /\s*\/\/\s*\[!code[^\]]*\].*$/gm; // Match ellipsis patterns indicating incomplete code: standalone "...", "// ...", or "/* ... */" @@ -78,7 +80,9 @@ function getMarkersBeforeBlock( const textBetween = lookbackText.substring(lastIndex); // Only apply if no other code block between marker and this block if (!textBetween.includes('```')) { - expectedErrors = expectMatch[1] + // Use whichever capture group matched (HTML or MDX format) + const errorCodes = expectMatch[1] || expectMatch[2]; + expectedErrors = errorCodes .split(',') .map((s) => parseInt(s.trim(), 10)) .filter((n) => !isNaN(n)); From 7bf8785451d5ced9adf6721585dce09d83e5e56c Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 17 Mar 2026 14:29:59 -0700 Subject: [PATCH 2/6] Enhance serialization docs: custom class examples with 'use step' instance methods - Add custom classes to Supported Serializable Types with anchor link - Replace verbose parenthetical in Requirements with back-link - Add [!code highlight] to Basic Example serde methods - Simplify geometry.ts to import Point instead of redefining it - Rewrite Complex Example to demonstrate 'use step' on instance methods with realistic APIs (database, HTTP) and contrast with workflow-context methods - Add pass-by-value note for 'this' context with return-and-reassign pattern --- .../docs/foundations/serialization.mdx | 161 ++++++++++++++---- 1 file changed, 125 insertions(+), 36 deletions(-) diff --git a/docs/content/docs/foundations/serialization.mdx b/docs/content/docs/foundations/serialization.mdx index 5e3459f998..ad0a4dcddb 100644 --- a/docs/content/docs/foundations/serialization.mdx +++ b/docs/content/docs/foundations/serialization.mdx @@ -56,6 +56,10 @@ These types have special handling and are explained in detail in the sections be - `ReadableStream` - `WritableStream` +**Custom Classes:** + +- Class instances that implement [`WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE`](#custom-class-serialization) + ## Streaming `ReadableStream` and `WritableStream` are supported as serializable types with special handling. These streams can be passed between workflow and step functions while maintaining their streaming capabilities. @@ -130,7 +134,7 @@ By default, custom class instances cannot be serialized because the serializatio {/* @expect-error:2351 */} ```typescript title="workflows/custom-class.ts" lineNumbers -import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; // [!code highlight] class Point { constructor( @@ -139,14 +143,14 @@ class Point { ) {} // Define how to serialize an instance to plain data - static [WORKFLOW_SERIALIZE](instance: Point) { - return { x: instance.x, y: instance.y }; - } + static [WORKFLOW_SERIALIZE](instance: Point) { // [!code highlight] + return { x: instance.x, y: instance.y }; // [!code highlight] + } // [!code highlight] // Define how to reconstruct an instance from plain data - static [WORKFLOW_DESERIALIZE](data: { x: number; y: number }) { - return new Point(data.x, data.y); - } + static [WORKFLOW_DESERIALIZE](data: { x: number; y: number }) { // [!code highlight] + return new Point(data.x, data.y); // [!code highlight] + } // [!code highlight] } ``` @@ -155,23 +159,14 @@ Once you've implemented these methods, instances of your class can be passed bet {/* @expect-error:2351 */} ```typescript title="workflows/geometry.ts" lineNumbers -import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; - -class Point { - constructor(public x: number, public y: number) {} - static [WORKFLOW_SERIALIZE](instance: Point) { - return { x: instance.x, y: instance.y }; - } - static [WORKFLOW_DESERIALIZE](data: { x: number; y: number }) { - return new Point(data.x, data.y); - } -} +import { Point } from "./custom-class"; export async function geometryWorkflow() { "use workflow"; const point = new Point(10, 20); - const doubled = await doublePoint(point); // Point is serialized automatically + // Point is serialized automatically + const doubled = await doublePoint(point); // [!code highlight] console.log(doubled.x, doubled.y); // 20, 40 return doubled; @@ -179,7 +174,8 @@ export async function geometryWorkflow() { async function doublePoint(point: Point) { "use step"; - return new Point(point.x * 2, point.y * 2); // Returns a new Point instance + // Returns a new Point instance + return new Point(point.x * 2, point.y * 2); // [!code highlight] } ``` @@ -197,7 +193,7 @@ async function doublePoint(point: Point) { Both methods must be implemented as **static** methods on the class. Instance methods are not supported. -- The data returned by `WORKFLOW_SERIALIZE` must itself be serializable (primitives, plain objects, arrays, or other serializable types including other custom classes) +- The data returned by `WORKFLOW_SERIALIZE` must itself be serializable (see [Supported Serializable Types](#supported-serializable-types)) - Both symbols must be implemented together - a class with only one will not be serializable @@ -211,12 +207,15 @@ Keep these methods simple and focused on data transformation only. ### Complex Example -Custom serialization works with nested objects and other serializable types: +A class that uses Node.js APIs or other non-deterministic operations cannot be used directly inside a workflow function. The recommended approach is to make the class workflow-compatible by adding `"use step"` to its instance methods. The SWC compiler will strip the method bodies from the workflow bundle and replace them with proxy functions that invoke the method as a step — with full Node.js runtime access. The `this` context (the class instance) is automatically serialized and deserialized across the workflow/step boundary. + +This requires the class to implement `WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE`, so that the instance can be passed to the step execution context. {/* @expect-error:2351 */} -```typescript title="workflows/complex-class.ts" lineNumbers -import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; +```typescript title="workflows/order.ts" lineNumbers +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; // [!code highlight] +import { db } from "../lib/db"; class Order { constructor( @@ -225,21 +224,111 @@ class Order { public createdAt: Date ) {} - static [WORKFLOW_SERIALIZE](instance: Order) { - return { - id: instance.id, - items: instance.items, // Map is serializable - createdAt: instance.createdAt, // Date is serializable - }; + // Custom serialization — data must be serializable types + static [WORKFLOW_SERIALIZE](instance: Order) { // [!code highlight] + return { // [!code highlight] + id: instance.id, // [!code highlight] + items: instance.items, // Map is serializable // [!code highlight] + createdAt: instance.createdAt, // Date is serializable // [!code highlight] + }; // [!code highlight] + } // [!code highlight] + + static [WORKFLOW_DESERIALIZE](data: { // [!code highlight] + id: string; // [!code highlight] + items: Map; // [!code highlight] + createdAt: Date; // [!code highlight] + }) { // [!code highlight] + return new Order(data.id, data.items, data.createdAt); // [!code highlight] + } // [!code highlight] + + // Methods without "use step" run in the workflow context + // and must follow the same constraints as workflow functions + total(): number { + let sum = 0; + for (const quantity of this.items.values()) { + sum += quantity; + } + return sum; } - static [WORKFLOW_DESERIALIZE](data: { - id: string; - items: Map; - createdAt: Date; - }) { - return new Order(data.id, data.items, data.createdAt); + // Instance methods with "use step" run as step functions + // with full Node.js access — `this` is automatically serialized + async save(): Promise { + "use step"; // [!code highlight] + await db.orders.insert({ // [!code highlight] + id: this.id, // [!code highlight] + items: Object.fromEntries(this.items), // [!code highlight] + createdAt: this.createdAt, // [!code highlight] + }); // [!code highlight] } + + async sendConfirmation(email: string): Promise { + "use step"; // [!code highlight] + const res = await fetch("https://api.example.com/email", { // [!code highlight] + method: "POST", // [!code highlight] + body: JSON.stringify({ // [!code highlight] + to: email, // [!code highlight] + orderId: this.id, // [!code highlight] + itemCount: this.items.size, // [!code highlight] + }), // [!code highlight] + }); // [!code highlight] + const { messageId } = await res.json(); + return messageId; + } +} +``` + +The class can then be used naturally inside a workflow function. Instance methods marked with `"use step"` are each executed as a step — with automatic caching, retry semantics, and full Node.js runtime access. Methods _without_ `"use step"` run directly in the workflow context, so they must follow the same constraints as workflow functions: + +{/* @expect-error:2351 */} + +```typescript title="workflows/process-order.ts" lineNumbers +export async function processOrderWorkflow( + orderId: string, + items: Map, + email: string +) { + "use workflow"; + + const order = new Order(orderId, items, new Date()); // [!code highlight] + + // Runs in the workflow context — no "use step" needed + const itemCount = order.total(); // [!code highlight] + + // Each "use step" instance method call runs as a separate step + await order.save(); // [!code highlight] + const messageId = await order.sendConfirmation(email); // [!code highlight] + + return { orderId, itemCount, messageId }; +} +``` + +Note that [pass-by-value semantics](#pass-by-value-semantics) also apply to the `this` context of `"use step"` instance methods. Modifying instance properties inside a step method will not affect the original instance in the workflow. If you need to update instance state, return `this` from the step method and re-assign the variable in the workflow: + +{/* @expect-error:2351 */} + +```typescript title="workflows/order.ts" lineNumbers +export class Order { + // ... + + async addItem(name: string, quantity: number): Promise { + "use step"; + this.items.set(name, quantity); + return this; // [!code highlight] + } +} +``` + +{/* @expect-error:2351 */} + +```typescript title="workflows/process-order.ts" lineNumbers +export async function processOrderWorkflow() { + "use workflow"; + + let order = new Order(orderId, items, new Date()); + + // Re-assign to capture the updated instance + order = await order.addItem("Widget", 3); // [!code highlight] } ``` From 29464749fdde09f6222e10544004e17a10222bb9 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 17 Mar 2026 14:35:23 -0700 Subject: [PATCH 3/6] Fix docs typecheck: use correct @expect-error codes for Order examples --- docs/content/docs/foundations/serialization.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/docs/foundations/serialization.mdx b/docs/content/docs/foundations/serialization.mdx index ad0a4dcddb..5b9ef0962d 100644 --- a/docs/content/docs/foundations/serialization.mdx +++ b/docs/content/docs/foundations/serialization.mdx @@ -280,7 +280,7 @@ class Order { The class can then be used naturally inside a workflow function. Instance methods marked with `"use step"` are each executed as a step — with automatic caching, retry semantics, and full Node.js runtime access. Methods _without_ `"use step"` run directly in the workflow context, so they must follow the same constraints as workflow functions: -{/* @expect-error:2351 */} +{/* @expect-error:2693 */} ```typescript title="workflows/process-order.ts" lineNumbers export async function processOrderWorkflow( @@ -319,7 +319,7 @@ export class Order { } ``` -{/* @expect-error:2351 */} +{/* @expect-error:2693,2552,2304 */} ```typescript title="workflows/process-order.ts" lineNumbers export async function processOrderWorkflow() { From 81c671e2c27546b8451371f7b796b153175c73b1 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 17 Mar 2026 14:41:52 -0700 Subject: [PATCH 4/6] Fix pre-existing docs typecheck failures in workflow-serde API reference Add @skip-typecheck to bare static method signatures that aren't valid standalone TypeScript. --- .../docs/api-reference/workflow-serde/workflow-deserialize.mdx | 2 ++ .../docs/api-reference/workflow-serde/workflow-serialize.mdx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx b/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx index 2abcbe2922..6b2e84846c 100644 --- a/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx +++ b/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx @@ -26,6 +26,8 @@ class Point { ## API Signature +{/* @skip-typecheck */} + ```typescript static [WORKFLOW_DESERIALIZE](data: SerializableData): T ``` diff --git a/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx b/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx index e7e4c6f10b..5c70c95e1d 100644 --- a/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx +++ b/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx @@ -26,6 +26,8 @@ class Point { ## API Signature +{/* @skip-typecheck */} + ```typescript static [WORKFLOW_SERIALIZE](instance: T): SerializableData ``` From 330ebcfb71e4a7a430471ca57dfe4098a52f5e4a Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 17 Mar 2026 16:19:28 -0700 Subject: [PATCH 5/6] Revert extractor changes, use HTML comments for @expect-error markers The docs typecheck extractor only supports HTML comment syntax for @expect-error on main. Use instead of {/* @expect-error:XXXX */} to match the existing convention. --- .../docs/api-reference/workflow-serde/index.mdx | 2 +- .../workflow-serde/workflow-deserialize.mdx | 2 +- .../workflow-serde/workflow-serialize.mdx | 2 +- docs/content/docs/foundations/serialization.mdx | 12 ++++++------ packages/docs-typecheck/src/extractor.ts | 8 ++------ 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/content/docs/api-reference/workflow-serde/index.mdx b/docs/content/docs/api-reference/workflow-serde/index.mdx index 80b377207f..4dc772f868 100644 --- a/docs/content/docs/api-reference/workflow-serde/index.mdx +++ b/docs/content/docs/api-reference/workflow-serde/index.mdx @@ -29,7 +29,7 @@ The `@workflow/serde` package provides two symbols that allow you to define cust ## Quick Example -{/* @expect-error:2351 */} + ```typescript lineNumbers import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; diff --git a/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx b/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx index 6b2e84846c..5e4ffc8c78 100644 --- a/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx +++ b/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx @@ -6,7 +6,7 @@ A symbol used to define custom deserialization for user-defined class instances. ## Usage -{/* @expect-error:2351 */} + ```typescript lineNumbers import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; diff --git a/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx b/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx index 5c70c95e1d..0b07a20be0 100644 --- a/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx +++ b/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx @@ -6,7 +6,7 @@ A symbol used to define custom serialization for user-defined class instances. T ## Usage -{/* @expect-error:2351 */} + ```typescript lineNumbers import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; diff --git a/docs/content/docs/foundations/serialization.mdx b/docs/content/docs/foundations/serialization.mdx index 5b9ef0962d..40b92ddc8e 100644 --- a/docs/content/docs/foundations/serialization.mdx +++ b/docs/content/docs/foundations/serialization.mdx @@ -131,7 +131,7 @@ By default, custom class instances cannot be serialized because the serializatio ### Basic Example -{/* @expect-error:2351 */} + ```typescript title="workflows/custom-class.ts" lineNumbers import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; // [!code highlight] @@ -156,7 +156,7 @@ class Point { Once you've implemented these methods, instances of your class can be passed between workflow and step functions: -{/* @expect-error:2351 */} + ```typescript title="workflows/geometry.ts" lineNumbers import { Point } from "./custom-class"; @@ -211,7 +211,7 @@ A class that uses Node.js APIs or other non-deterministic operations cannot be u This requires the class to implement `WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE`, so that the instance can be passed to the step execution context. -{/* @expect-error:2351 */} + ```typescript title="workflows/order.ts" lineNumbers import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; // [!code highlight] @@ -280,7 +280,7 @@ class Order { The class can then be used naturally inside a workflow function. Instance methods marked with `"use step"` are each executed as a step — with automatic caching, retry semantics, and full Node.js runtime access. Methods _without_ `"use step"` run directly in the workflow context, so they must follow the same constraints as workflow functions: -{/* @expect-error:2693 */} + ```typescript title="workflows/process-order.ts" lineNumbers export async function processOrderWorkflow( @@ -305,7 +305,7 @@ export async function processOrderWorkflow( Note that [pass-by-value semantics](#pass-by-value-semantics) also apply to the `this` context of `"use step"` instance methods. Modifying instance properties inside a step method will not affect the original instance in the workflow. If you need to update instance state, return `this` from the step method and re-assign the variable in the workflow: -{/* @expect-error:2351 */} + ```typescript title="workflows/order.ts" lineNumbers export class Order { @@ -319,7 +319,7 @@ export class Order { } ``` -{/* @expect-error:2693,2552,2304 */} + ```typescript title="workflows/process-order.ts" lineNumbers export async function processOrderWorkflow() { diff --git a/packages/docs-typecheck/src/extractor.ts b/packages/docs-typecheck/src/extractor.ts index 729b9f69c7..2be7b306a3 100644 --- a/packages/docs-typecheck/src/extractor.ts +++ b/packages/docs-typecheck/src/extractor.ts @@ -2,9 +2,7 @@ import type { CodeSample } from './types.js'; const CODE_BLOCK_REGEX = /```(typescript|ts|javascript|js)(?:[^\S\n]+[^\n]*)?\n([\s\S]*?)```/g; -// Support both HTML comments () and MDX comments ({/* @expect-error:2351 */}) -const EXPECT_ERROR_REGEX = - /(?:|\{\/\*\s*@expect-error:([0-9,\s]+)\s*\*\/\})/; +const EXPECT_ERROR_REGEX = //; // Match entire line comments with [!code ...] including any trailing text const HIGHLIGHT_COMMENT_REGEX = /\s*\/\/\s*\[!code[^\]]*\].*$/gm; // Match ellipsis patterns indicating incomplete code: standalone "...", "// ...", or "/* ... */" @@ -80,9 +78,7 @@ function getMarkersBeforeBlock( const textBetween = lookbackText.substring(lastIndex); // Only apply if no other code block between marker and this block if (!textBetween.includes('```')) { - // Use whichever capture group matched (HTML or MDX format) - const errorCodes = expectMatch[1] || expectMatch[2]; - expectedErrors = errorCodes + expectedErrors = expectMatch[1] .split(',') .map((s) => parseInt(s.trim(), 10)) .filter((n) => !isNaN(n)); From 9830710c0720eef43f471372b95de313e72c863a Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 17 Mar 2026 16:51:30 -0700 Subject: [PATCH 6/6] Use MDX comment syntax for @expect-error markers, add MDX support to extractor MDX files don't support HTML comments (). The extractor's @expect-error regex only supported HTML syntax, but @skip-typecheck already supported both. This aligns @expect-error to also support the MDX comment syntax ({/* @expect-error:XXXX */}). --- .../docs/api-reference/workflow-serde/index.mdx | 2 +- .../workflow-serde/workflow-deserialize.mdx | 2 +- .../workflow-serde/workflow-serialize.mdx | 2 +- docs/content/docs/foundations/serialization.mdx | 12 ++++++------ packages/docs-typecheck/src/extractor.ts | 8 ++++++-- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/content/docs/api-reference/workflow-serde/index.mdx b/docs/content/docs/api-reference/workflow-serde/index.mdx index 4dc772f868..80b377207f 100644 --- a/docs/content/docs/api-reference/workflow-serde/index.mdx +++ b/docs/content/docs/api-reference/workflow-serde/index.mdx @@ -29,7 +29,7 @@ The `@workflow/serde` package provides two symbols that allow you to define cust ## Quick Example - +{/* @expect-error:2351 */} ```typescript lineNumbers import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; diff --git a/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx b/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx index 5e4ffc8c78..6b2e84846c 100644 --- a/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx +++ b/docs/content/docs/api-reference/workflow-serde/workflow-deserialize.mdx @@ -6,7 +6,7 @@ A symbol used to define custom deserialization for user-defined class instances. ## Usage - +{/* @expect-error:2351 */} ```typescript lineNumbers import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; diff --git a/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx b/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx index 0b07a20be0..5c70c95e1d 100644 --- a/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx +++ b/docs/content/docs/api-reference/workflow-serde/workflow-serialize.mdx @@ -6,7 +6,7 @@ A symbol used to define custom serialization for user-defined class instances. T ## Usage - +{/* @expect-error:2351 */} ```typescript lineNumbers import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; diff --git a/docs/content/docs/foundations/serialization.mdx b/docs/content/docs/foundations/serialization.mdx index 40b92ddc8e..5b9ef0962d 100644 --- a/docs/content/docs/foundations/serialization.mdx +++ b/docs/content/docs/foundations/serialization.mdx @@ -131,7 +131,7 @@ By default, custom class instances cannot be serialized because the serializatio ### Basic Example - +{/* @expect-error:2351 */} ```typescript title="workflows/custom-class.ts" lineNumbers import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; // [!code highlight] @@ -156,7 +156,7 @@ class Point { Once you've implemented these methods, instances of your class can be passed between workflow and step functions: - +{/* @expect-error:2351 */} ```typescript title="workflows/geometry.ts" lineNumbers import { Point } from "./custom-class"; @@ -211,7 +211,7 @@ A class that uses Node.js APIs or other non-deterministic operations cannot be u This requires the class to implement `WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE`, so that the instance can be passed to the step execution context. - +{/* @expect-error:2351 */} ```typescript title="workflows/order.ts" lineNumbers import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde"; // [!code highlight] @@ -280,7 +280,7 @@ class Order { The class can then be used naturally inside a workflow function. Instance methods marked with `"use step"` are each executed as a step — with automatic caching, retry semantics, and full Node.js runtime access. Methods _without_ `"use step"` run directly in the workflow context, so they must follow the same constraints as workflow functions: - +{/* @expect-error:2693 */} ```typescript title="workflows/process-order.ts" lineNumbers export async function processOrderWorkflow( @@ -305,7 +305,7 @@ export async function processOrderWorkflow( Note that [pass-by-value semantics](#pass-by-value-semantics) also apply to the `this` context of `"use step"` instance methods. Modifying instance properties inside a step method will not affect the original instance in the workflow. If you need to update instance state, return `this` from the step method and re-assign the variable in the workflow: - +{/* @expect-error:2351 */} ```typescript title="workflows/order.ts" lineNumbers export class Order { @@ -319,7 +319,7 @@ export class Order { } ``` - +{/* @expect-error:2693,2552,2304 */} ```typescript title="workflows/process-order.ts" lineNumbers export async function processOrderWorkflow() { diff --git a/packages/docs-typecheck/src/extractor.ts b/packages/docs-typecheck/src/extractor.ts index 2be7b306a3..729b9f69c7 100644 --- a/packages/docs-typecheck/src/extractor.ts +++ b/packages/docs-typecheck/src/extractor.ts @@ -2,7 +2,9 @@ import type { CodeSample } from './types.js'; const CODE_BLOCK_REGEX = /```(typescript|ts|javascript|js)(?:[^\S\n]+[^\n]*)?\n([\s\S]*?)```/g; -const EXPECT_ERROR_REGEX = //; +// Support both HTML comments () and MDX comments ({/* @expect-error:2351 */}) +const EXPECT_ERROR_REGEX = + /(?:|\{\/\*\s*@expect-error:([0-9,\s]+)\s*\*\/\})/; // Match entire line comments with [!code ...] including any trailing text const HIGHLIGHT_COMMENT_REGEX = /\s*\/\/\s*\[!code[^\]]*\].*$/gm; // Match ellipsis patterns indicating incomplete code: standalone "...", "// ...", or "/* ... */" @@ -78,7 +80,9 @@ function getMarkersBeforeBlock( const textBetween = lookbackText.substring(lastIndex); // Only apply if no other code block between marker and this block if (!textBetween.includes('```')) { - expectedErrors = expectMatch[1] + // Use whichever capture group matched (HTML or MDX format) + const errorCodes = expectMatch[1] || expectMatch[2]; + expectedErrors = errorCodes .split(',') .map((s) => parseInt(s.trim(), 10)) .filter((n) => !isNaN(n));