ViteHub

Workflow usage

Practical patterns for typed payloads, workflow starts, deferred dispatch, stable run ids, and status checks.

After the quickstart works, most Workflow code falls into four patterns: define a typed flow, start it from request code, choose whether dispatch is awaited or deferred, and check the normalized run later.

Create explicit workflow handles

Use createWorkflow(name, handler) when explicit integration code constructs the workflow handle itself. Framework-discovered workflows should use files or folders; discovery does not scan createWorkflow() calls.

import { createWorkflow } from '@vitehub/workflow'

type WelcomePayload = {
  email: string
  marker?: string
}

type WelcomeResult = {
  message: string
  marker?: string
}

export const welcomeWorkflow = createWorkflow<WelcomePayload, WelcomeResult>('welcome', async ({ payload }) => {
  return {
    message: `Welcome ${payload.email}`,
    marker: payload.marker,
  }
})

Define discovered workflows

Use discovered workflow files or folders when the workflow should live outside the caller.

src/welcome.workflow.ts
import { defineWorkflow } from '@vitehub/workflow'

export type WelcomePayload = {
  email: string
  marker?: string
}

export type WelcomeResult = {
  message: string
  marker?: string
}

export default defineWorkflow<WelcomePayload, WelcomeResult>(async ({ payload }) => {
  return {
    message: `Welcome ${payload.email}`,
    marker: payload.marker,
  }
})

The handler receives a WorkflowExecutionContext<TPayload> with id, name, payload, provider, and provider-owned step fields.

For multi-step Nitro workflows, create a folder:

server/workflows/import-products/
  index.ts
  01.extract.ts
  02.transform.ts

If index.ts is missing, step files run in sorted order and each result is passed to the next step.

Keep producers provider-neutral

The route should not know whether Cloudflare or Vercel is running the workflow.

const run = await welcomeWorkflow.run(payload)

Provider details belong in config:

workflow: {
  provider: 'cloudflare',
}

Start with a bare payload

Pass the payload directly when you do not need a custom run id:

await welcomeWorkflow.run({
  email: 'ava@example.com',
  marker: 'signup-42',
})

runWorkflow() resolves after the provider accepts the start:

{
  "id": "wrun_lvn4hx4f_x8k2p9s1",
  "provider": "cloudflare",
  "status": "queued"
}

Start with a stable run id

Pass the id in options when the caller needs to persist or poll a known id:

await welcomeWorkflow.run(payload, { id: 'welcome-signup-42' })

When stable identity belongs to the workflow, define it once on the handle:

const welcomeWorkflow = createWorkflow<WelcomePayload, WelcomeResult>('welcome', async ({ payload }) => {
  return await sendWelcome(payload)
}, {
  id: ({ payload }) => ({
    email: payload?.email,
    marker: payload?.marker,
  }),
})

await welcomeWorkflow.run(payload)

Defer dispatch until after the response

Use deferWorkflow() when the route should return immediately and start dispatch can run through the current runtime context:

import { createWorkflow } from '@vitehub/workflow'

const welcomeWorkflow = createWorkflow('welcome')

export default defineEventHandler(async () => {
  await welcomeWorkflow.defer({ email: 'ava@example.com', marker: 'signup-42' })
  return { ok: true }
})

deferWorkflow() returns the start promise and uses waitUntil() when the runtime provides it.

Observe a run

Use the workflow handle with the run id:

const run = await welcomeWorkflow.getRun(id)

if (run.status === 'completed') {
  console.log(run.result?.message)
}

Normalized status values are queued, running, completed, failed, and unknown.

Provider support differs:

ProviderStatus behavior
CloudflareReads the Workflow binding when available and normalizes provider status metadata.
VercelReports generated runtime state for runs started by the same deployment process.
OpenWorkflowReads durable run state from the configured Postgres backend.

Validate request payloads

Use readValidatedPayload() when a Web Request should be parsed and validated before starting the workflow:

import { readValidatedPayload, runWorkflow } from '@vitehub/workflow'
import { z } from 'zod'

const welcomePayload = z.object({
  email: z.string().email(),
  marker: z.string().optional(),
})

export default defineEventHandler(async (event) => {
  const payload = await readValidatedPayload(event.req, welcomePayload)

  return {
    ok: true,
    run: await runWorkflow('welcome', payload),
  }
})

readValidatedPayload() accepts a parser function, a schema with parse(), or a schema with safeParse().

Workflow naming rules

Workflow names always come from discovered files:

  • src/welcome.workflow.ts becomes welcome
  • src/email/welcome.workflow.ts becomes email/welcome
  • server/workflows/welcome.ts becomes welcome

Next steps

Copyright © 2026