@voyant-travel/workflow-runs
v0.111.9
Published
Workflow run recording, admin routes, and rerun/resume dispatch primitives for Voyant operator apps.
Readme
@voyant-travel/workflow-runs
Workflow run recording, admin routes, and rerun/resume dispatch primitives for Voyant operator apps.
Install
pnpm add @voyant-travel/workflow-runsThe package is independently published with the rest of the workflows release cohort.
For the matching importable React admin surface, install
@voyant-travel/workflows-react/ui and point its API client at the routes mounted by
this package:
import {
createWorkflowRunsApiClient,
WorkflowRunsPage,
} from "@voyant-travel/workflows-react/ui"
const workflowRunsApi = createWorkflowRunsApiClient({ apiBase: "/api" })
export function WorkflowsRoute() {
return <WorkflowRunsPage api={workflowRunsApi} />
}Mount the admin routes
mountWorkflowRunsAdminRoutes adds the workflow-run list, detail, rerun, and resume endpoints under /v1/admin/workflow-runs, plus an explicit trigger endpoint at POST /v1/admin/workflows/:name/runs.
import { mountWorkflowRunsAdminRoutes, WorkflowRunnerRegistry } from "@voyant-travel/workflow-runs"
import { createSelfHostWorkflowClient } from "@voyant-travel/workflows-orchestrator/selfhost"
const workflowRunnerRegistry = new WorkflowRunnerRegistry()
const workflowServer = createSelfHostWorkflowClient({
baseUrl: process.env.WORKFLOW_SERVER_URL!,
})
workflowRunnerRegistry.register({
name: "checkout-finalize",
idempotency: "unsafe",
description:
"Confirms the booking and issues the final invoice. Use Resume to retry from a failed step.",
trigger: async (input, ctx) => {
const saved = await workflowServer.trigger({
workflowId: "checkout-finalize",
input,
tags: ctx.tags,
triggeredByUserId: ctx.triggeredByUserId,
})
return { runId: saved.id }
},
rerun: async (input, ctx) => {
const saved = await workflowServer.trigger({
workflowId: "checkout-finalize",
input,
tags: [...ctx.tags, "rerun:true"],
})
return { runId: saved.id }
},
resume: async (input, ctx) => {
const { saved } = await workflowServer.resume(ctx.parentRunId, {
workflowId: "checkout-finalize",
input,
resumeFromStep: ctx.resumeFromStep,
seedResults: ctx.seedResults,
tags: [...ctx.tags, "resume:true"],
triggeredByUserId: ctx.triggeredByUserId,
})
return { runId: saved.id }
},
})
mountWorkflowRunsAdminRoutes(hono, {
runners: workflowRunnerRegistry,
adminSurface: process.env.VOYANT_WORKFLOW_ADMIN_SURFACE as
| "tenant"
| "cloud"
| "disabled"
| undefined,
})adminSurface controls tenant-admin workflow management actions:
tenantkeeps local/self-host trigger, rerun, and resume routes available.cloudkeeps read routes available but rejects tenant-admin trigger, rerun, and resume routes because the Voyant Cloud dashboard is the workflow control plane for managed deployments.disabledkeeps read routes available and disables tenant-admin management actions completely.
When omitted, the package reads VOYANT_WORKFLOW_ADMIN_SURFACE. If that is
unset but managed Cloud workflow env is present, the default is cloud;
otherwise the default is tenant for local/self-host compatibility.
Triggerable workflows must opt in by implementing trigger(...) on their registered runner. This keeps rerun/resume-only workflows closed to arbitrary admin dispatch while still allowing operators, cron jobs, queues, and API keys with workflows:trigger permission to call:
POST /v1/admin/workflows/checkout-finalize/runs
Content-Type: application/json
{
"input": { "bookingId": "bk_123" },
"idempotencyKey": "checkout-finalize:bk_123",
"correlationId": "bk_123",
"tags": ["source:admin"]
}The route returns 202 Accepted with the queued run id:
{
"data": {
"runId": "wfrn_...",
"workflowName": "checkout-finalize",
"status": "queued"
}
}For self-hosted workflow services, keep runner registration close to the code that mounts the workflow service. The registry should dispatch to your external workflow server instead of importing worker-only runtime code into the admin API process. The resume path sends ctx.resumeFromStep plus ctx.seedResults; the self-host server starts a new run, pre-populates the journal with the seeded step outputs, and executes from the failed step onward.
Record @voyant-travel/workflows executions
Use recordedWorkflow as a drop-in replacement for workflow(...) when a
workflow should appear in the workflow runs admin UI. The helper records start,
success, and failure rows in workflow_runs without repeating recorder
boilerplate in every workflow body.
import { recordedWorkflow } from "@voyant-travel/workflow-runs"
export const generatePdfWorkflow = recordedWorkflow({
id: "products.generate-pdf",
tags: ["products"],
async run(input, ctx) {
const renderer = ctx.services.resolve("products:pdf-renderer")
return renderer.generate(input)
},
})By default, recordedWorkflow resolves a Drizzle database from
ctx.services.resolve("db"). It records the workflow id, trigger, run id as the
correlation id, configured/runtime tags, input, result, parent run id for child
workflow triggers, and errors. Recording is best-effort: database or serializer
failures do not fail the workflow execution.
You can customize the database service key or payload serializers:
export const syncCatalogWorkflow = recordedWorkflow(
{
id: "catalog.sync",
async run(input, ctx) {
return ctx.services.resolve("catalog:sync").run(input)
},
},
{
dbServiceName: "postgres",
input: ({ input }) => ({ catalogId: input.catalogId }),
result: ({ output }) => ({ changed: output.changed }),
},
)This helper only records observability data. Trigger, rerun, and resume support
still uses WorkflowRunnerRegistry registration so apps can choose which
workflows are safe to dispatch from the admin UI.
