defineHook

Create type-safe hooks with consistent payload types and optional validation.

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() and resumeHook() to avoid type mismatches. It also supports optional runtime validation and transformation of payloads using any Standard Schema v1 compliant validator like Zod or Valibot.

When a schema is provided, the hook is typed as TypedHook<TInput, TOutput> — the resume() method accepts TInput (the raw payload), while the workflow receives TOutput (the validated and potentially transformed result). Without a schema, TOutput defaults to TInput.

defineHook() spans both execution contexts: call .create() inside "use workflow" code to create the hook, and call .resume() from runtime code (such as API routes or server actions) to resume it. Calling .create() outside a workflow or .resume() inside one will throw an error.

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

import { defineHook } from "workflow";

const nameHook = defineHook<{
  name: string;
}>();

export async function nameWorkflow() {
  "use workflow";

  const hook = nameHook.create();  
  const result = await hook; // Fully typed as { name: string }
  console.log("Name:", result.name);
}

API Signature

Parameters

NameTypeDescription
__0{ schema?: StandardSchemaV1<TInput, TOutput>; }Schema used to validate and transform the input payload before resuming

Returns

NameTypeDescription
create(options?: any) => Hook<TOutput> Creates a new hook with the defined output type.
resume(token: string, payload: TInput) => Promise<HookEntity> Resumes a hook by sending a payload with the defined input type. Throws an error if the hook is not found or if schema validation fails.

Examples

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.

import { defineHook } from "workflow";

// Define once with a specific payload type
const approvalHook = defineHook<{ 
  approved: boolean; 
  comment: string; 
}>(); 

// In your workflow
export async function workflowWithApproval() {
  "use workflow";

  const hook = approvalHook.create();
  const result = await hook; // Fully typed as { approved: boolean; comment: string }

  console.log("Approved:", result.approved);
  console.log("Comment:", result.comment);
}

Resuming with Type Safety

Hooks can be resumed using the same defined hook and a token. By using the same hook, you can ensure that the payload matches the defined type when resuming a hook. The resume() method throws an error if the hook is not found or if schema validation fails.

// Use the same defined hook to resume
export async function POST(request: Request) {
  const { token, approved, comment } = await request.json();

  try {
    // Type-safe resumption - TypeScript ensures the payload matches
    const result = await approvalHook.resume(token, { 
      approved, 
      comment, 
    }); 

    return Response.json({ success: true, runId: result.runId });
  } catch (error) {
    return Response.json({ error: "Hook not found or invalid payload" }, { status: 400 });
  }
}

Validate and Transform with Schema

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

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.

Using Zod with defineHook

Here's an example using Zod to validate and transform hook payloads:

import { defineHook } from "workflow";
import { z } from "zod";

export const approvalHook = defineHook({
  schema: z.object({ 
    approved: z.boolean(), 
    comment: z.string().min(1).transform((value) => value.trim()), 
  }), 
});

export async function approvalWorkflow(approvalId: string) {
  "use workflow";

  const hook = approvalHook.create({
    token: `approval:${approvalId}`,
  });

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

When resuming the hook from an API route, the schema validates and transforms the incoming payload before the workflow resumes:

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

  // 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, { 
    approved, 
    comment, // Automatically trimmed to "Ready!"
  }); 

  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:

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

export const approvalHook = defineHook({
  schema: v.object({ 
    approved: v.boolean(), 
    comment: v.pipe(v.string(), v.minLength(1), v.trim()), 
  }), 
});

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.

import { defineHook } from "workflow";

const slackHook = defineHook<{ text: string; userId: string }>();

export async function slackBotWorkflow(channelId: string) {
  "use workflow";

  const hook = slackHook.create({
    token: `slack:${channelId}`, 
  });

  const message = await hook;
  console.log(`Message from ${message.userId}: ${message.text}`);
}