Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/young-humans-create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
48 changes: 37 additions & 11 deletions docs/content/docs/api-reference/workflow/define-hook.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import { generateDefinition } from "@/lib/tsdoc"

Creates a type-safe hook helper that ensures the payload type is consistent between hook creation and resumption.

This is a lightweight wrapper around [`createHook()`](/docs/api-reference/workflow/create-hook) and [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid type mismatches.
This is a lightweight wrapper around [`createHook()`](/docs/api-reference/workflow/create-hook) and [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid type mismatches. It also supports optional runtime validation and transformation of payloads using any [Standard Schema v1](https://standardschema.dev) compliant validator like Zod or Valibot.

<Callout>
We recommend using `defineHook()` over `createHook()` in production codebases for better type safety.
We recommend using `defineHook()` over `createHook()` in production codebases for better type safety and optional runtime validation.
</Callout>

```ts lineNumbers
Expand Down Expand Up @@ -67,7 +67,7 @@ export default DefineHook;`

## Examples

### Type-Safe Hook Definition
### Basic Type-Safe Hook Definition

By defining the hook once with a specific payload type, you can reuse it in multiple workflows and API routes with automatic type safety.

Expand Down Expand Up @@ -117,16 +117,21 @@ export async function POST(request: Request) {

### Validate and Transform with Schema

The optional `schema` accepts any validator that conforms to [Standard Schema v1](https://standardschema.dev).
You can provide runtime validation and transformation of hook payloads using the `schema` option. This option accepts any validator that conforms to the [Standard Schema v1](https://standardschema.dev) specification.

Zod is shown below as one example, but libraries like Valibot, ArkType, Effect Schema, or your own custom validator work as well.
<Callout type="info">
Standard Schema is a standardized specification for schema validation libraries. Most popular validation libraries support it, including Zod, Valibot, ArkType, and Effect Schema. You can also write custom validators.
</Callout>

#### Using Zod with defineHook

Here's an example using [Zod](https://zod.dev) to validate and transform hook payloads:

```typescript lineNumbers
import { defineHook } from "workflow";
import { z } from "zod";

export const approvalHook = defineHook({
// Provide a schema to validate/transform payloads.
schema: z.object({ // [!code highlight]
approved: z.boolean(), // [!code highlight]
comment: z.string().min(1).transform((value) => value.trim()), // [!code highlight]
Expand All @@ -140,29 +145,50 @@ export async function approvalWorkflow(approvalId: string) {
token: `approval:${approvalId}`,
});

// Payload is automatically typed based on the schema
const { approved, comment } = await hook;
console.log('Approved:', approved);
console.log('Comment:', comment);
console.log('Comment (trimmed):', comment);
}
```

In your route handler, resume the hook with the same definition; the schema validates and transforms the payload before the workflow continues.
When resuming the hook from an API route, the schema validates and transforms the incoming payload before the workflow resumes:

```typescript lineNumbers
export async function POST(request: Request) {
// comment is " Ready! " here
// Incoming payload: { token: "...", approved: true, comment: " Ready! " }
const { token, approved, comment } = await request.json();

// If validation fails, Zod throws and the hook is not resumed.
// The schema validates and transforms the payload:
// - Checks that `approved` is a boolean
// - Checks that `comment` is a non-empty string
// - Trims whitespace from the comment
// If validation fails, an error is thrown and the hook is not resumed
await approvalHook.resume(token, { // [!code highlight]
approved, // [!code highlight]
comment, // transformed to "Ready!" [!code highlight]
comment, // Automatically trimmed to "Ready!" // [!code highlight]
}); // [!code highlight]

return Response.json({ success: true });
}
```

#### Using Other Standard Schema Libraries

The same pattern works with any Standard Schema v1 compliant library. Here's an example with [Valibot](https://valibot.dev):

```typescript lineNumbers
import { defineHook } from "workflow";
import * as v from "valibot";

export const approvalHook = defineHook({
schema: v.object({ // [!code highlight]
approved: v.boolean(), // [!code highlight]
comment: v.pipe(v.string(), v.minLength(1), v.trim()), // [!code highlight]
}), // [!code highlight]
});
```

### Customizing Tokens

Tokens are used to identify a specific hook and for resuming a hook. You can customize the token to be more specific to a use case.
Expand Down
30 changes: 16 additions & 14 deletions docs/content/docs/foundations/hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -362,20 +362,21 @@ async function postToSlack(channelId: string, message: string) {

### Type-Safe Hooks with `defineHook()`

The [`defineHook()`](/docs/api-reference/workflow/define-hook) helper provides type safety between creating and resuming hooks:
The [`defineHook()`](/docs/api-reference/workflow/define-hook) helper provides type safety and runtime validation between creating and resuming hooks using [Standard Schema v1](https://standardschema.dev). Use any compliant validator like Zod or Valibot:

```typescript lineNumbers
import { defineHook } from "workflow";

// Define the hook type once
type ApprovalRequest = {
requestId: string;
approved: boolean;
approvedBy: string;
comment: string;
};

const approvalHook = defineHook<ApprovalRequest>();
import { z } from "zod";

// Define the hook with schema for type safety and runtime validation
const approvalHook = defineHook({ // [!code highlight]
schema: z.object({ // [!code highlight]
requestId: z.string(), // [!code highlight]
approved: z.boolean(), // [!code highlight]
approvedBy: z.string(), // [!code highlight]
comment: z.string().transform((value) => value.trim()), // [!code highlight]
}), // [!code highlight]
}); // [!code highlight]

// In your workflow
export async function documentApprovalWorkflow(documentId: string) {
Expand All @@ -385,24 +386,25 @@ export async function documentApprovalWorkflow(documentId: string) {
token: `approval:${documentId}`
});

// Payload is type-safe and validated
const approval = await hook;

console.log(`Document ${approval.requestId} ${approval.approved ? "approved" : "rejected"}`);
console.log(`By: ${approval.approvedBy}, Comment: ${approval.comment}`);
}

// In your API route - TypeScript ensures the payload matches!
// In your API route - both type-safe and runtime-validated!
export async function POST(request: Request) {
const { documentId, ...approvalData } = await request.json();

// This is type-safe - TypeScript knows the exact shape required
// The schema validates the payload before resuming the workflow
await approvalHook.resume(`approval:${documentId}`, approvalData);

return new Response("OK");
}
```

This pattern is especially valuable in larger applications where the workflow and API code are in separate files.
This pattern is especially valuable in larger applications where the workflow and API code are in separate files, providing both compile-time type safety and runtime validation.

## Best Practices

Expand Down