Workflow

Run durable workflows on Cloudflare, Netlify, Vercel, or OpenWorkflow.

Use Workflow to run durable server-side processes that can pause, retry, checkpoint, and outlive a single request.

Create workflow files under server/workflows/ with defineWorkflow(handler, options?). Start them with runWorkflow(). When your provider supports it, use getWorkflowRun() later to check the same run again.

Getting started

Install the package

Terminal
pnpm add https://pkg.pr.new/vite-hub/vitehub/@vitehub/workflow@main

Configure a provider

On Cloudflare, Netlify, and Vercel, ViteHub usually picks the workflow provider automatically from the hosting platform. Set workflow.provider when you want to override that choice or when you use OpenWorkflow. On Cloudflare, set workflow.binding only when you need a different binding name or you want to pass the binding object directly.

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@vitehub/workflow/nuxt'],
})

Define a workflow

Create the workflow file for the job you want to run. ViteHub uses the file path as the workflow name, so server/workflows/publish-draft.ts registers publish-draft.

server/workflows/publish-draft.ts
import { defineWorkflow, readValidatedPayload } from '@vitehub/workflow'

export default defineWorkflow(async (input?: { title?: string }) => {
  const { title } = await readValidatedPayload(input, value => ({
    title: typeof value?.title === 'string' && value.title.trim()
      ? value.title.trim()
      : 'Untitled draft',
  }))

  return {
    title,
    slug: title.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
  }
})

Start a run

server/api/workflows/publish.post.ts
import { createError, readBody } from 'h3'
import { readValidatedPayload, runWorkflow } from '@vitehub/workflow'

export default defineEventHandler(async (event) => {
  const { title } = await readValidatedPayload(await readBody(event), (input) => {
    if (!input || typeof input !== 'object' || typeof input.title !== 'string' || !input.title.trim()) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Enter a title for the draft.',
      })
    }

    return {
      title: input.title.trim(),
    }
  })
  const run = await runWorkflow('publish-draft', { title })

  return {
    runId: run.id,
    status: await run.status(),
  }
})

Reattach to the same run later

Persist or return the run id when you start a workflow. Use it later to check status or call provider-specific run methods.

server/api/workflows/[id].get.ts
import { getRouterParam } from 'h3'
import { getWorkflowRun } from '@vitehub/workflow'

export default defineEventHandler(async (event) => {
  const run = await getWorkflowRun(getRouterParam(event, 'id'))

  if (!run)
    return { found: false }

  return {
    found: true,
    status: await run.status(),
  }
})
Cloudflare Workflow
Configure Cloudflare Workflows and inspect runs from your app.
Vercel Workflow
Configure Vercel Workflow and run durable workflows on Vercel.
OpenWorkflow Workflow
Connect OpenWorkflow and use your own workflow backend behind the ViteHub API.

Defer a workflow run

Use deferWorkflow() when you want to start durable workflow work after the current response has already been committed. It reads Nitro's current request context internally, prefers a real waitUntil() hook when one exists, and otherwise falls back to best-effort fire-and-forget dispatch.

import { deferWorkflow } from '@vitehub/workflow'

export default defineEventHandler(() => {
  deferWorkflow('publish-draft', {
    title: 'Hello from waitUntil',
  })

  return { ok: true }
})

Public API

FunctionUse it for

| defineWorkflow(handler, options?) | Register one named workflow from a file under server/workflows/. |

| runWorkflow(name, payload) | Start a new workflow run and get a run handle back immediately. | | getWorkflowRun(runId) | Reattach to an existing run from a later request, task, or webhook. |

Type reference

Handler signature

The handler receives an optional input and returns the workflow result. ViteHub does not enforce a return type.

type WorkflowHandler<TInput, TResult> = (
  input?: TInput,
  context?: import('@vitehub/workflow').WorkflowExecutionContext,
) => TResult | Promise<TResult>

WorkflowExecutionContext

Handlers receive a portable execution context as the second argument.

FieldTypeDescription
providerWorkflowProviderActive workflow provider name.
stepWorkflowStepContext | undefinedPortable step helpers. Implemented by Netlify in this release.
eventIdstring | undefinedProvider-native event or run identifier when available.
attemptnumber | undefinedRetry attempt when the provider exposes it.
sendEvent((eventName, options?) => Promise<WorkflowSendEventResult>) | undefinedProvider-backed event publishing helper when available.

WorkflowDefinitionOptions

The options object passed to defineWorkflow() accepts:

OptionTypeDescription
timeoutnumberMaximum execution time in milliseconds.

WorkflowRun

runWorkflow() returns a WorkflowRun handle with:

Field / MethodTypeDescription
idstringUnique run identifier. Persist this to reattach later.
providerstringThe active provider name.
nativeobjectRaw provider run object from the active SDK or platform binding.
status()Promise<WorkflowRunStatus>Poll the current run state.
stop()Promise<void>Cancel the run.

WorkflowRunStatus

FieldTypeDescription
state'queued' | 'running' | 'paused' | 'waiting' | 'completed' | 'failed' | 'cancelled' | 'terminated' | 'unknown'Current run state.
rawunknownRaw provider response.
outputunknownHandler return value when the run completes.
errorunknownError details when the run fails.

Configure provider and workflow options

Workflow has one global layer and one definition layer:

  • Top-level workflow config in nuxt.config.ts or nitro.config.ts selects the provider and sets up the app-wide integration.
  • createWorkflow(options?)(handler) and defineWorkflow(handler, options?) both configure one workflow definition with portable options such as timeout. ::
  • defineWorkflow(handler, options?) configures one workflow definition with portable options such as timeout.
  • Netlify ignores definition-level timeout for now and executes workflows through Async Workloads event handlers.

runWorkflow() starts a new run with input for that specific execution. getWorkflowRun() reconnects to an existing run by id. Neither one replaces top-level or definition-level configuration.

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@vitehub/workflow/nuxt'],
  workflow: {
    provider: 'cloudflare',
  },
})
server/workflows/publish-draft.ts
export default defineWorkflow(async (input) => {
  return input
})
Each provider adds its own runtime behavior and extra methods. Use the provider pages in the sidebar for binding setup, backend clients, and provider-specific run helpers.ViteHub only requires the SDK for the provider you choose. Cloudflare stays SDK-free, Vercel needs workflow, and OpenWorkflow needs openworkflow.