UNPKG

@convex-dev/workflow

Version:

Convex component for durably executing workflows.

458 lines 16.8 kB
import { parse } from "convex-helpers/validators"; import { createFunctionHandle, } from "convex/server"; import { safeFunctionName } from "./safeFunctionName.js"; import { workflowMutation } from "./workflowMutation.js"; export { vEventId, vWorkflowId, vWorkflowStep, } from "../types.js"; export { vResultValidator } from "@convex-dev/workpool"; /** * Define a new workflow with typed args and optional return validator. * * @example * ```ts * export const myWorkflow = defineWorkflow(components.workflow, { * args: { amount: v.number() }, * returns: v.object({ total: v.number() }), * }).handler(async (step, args) => { * ...workflow implementation * }); * ``` * * Start the workflow from a mutation or action: * ```ts * const workflowId = await start(ctx, internal.myFile.myWorkflow, { amount: 42 }); * ``` * Or call it directly: * ```ts * const workflowId = await ctx.runMutation(internal.myFile.myWorkflow, { args: { ...myArgs } }); * ``` */ export function defineWorkflow(component, config) { return { handler: (fn) => workflowMutation(component, { ...config, handler: fn }, undefined), }; } /** * Start a workflow * * It will run asynchronously, returning a workflow ID to monitor the progress. * * By default it will start running the handler as part of "start" unless * `startAsync` is set to true. * * ```ts * const id = await start(ctx, internal.myFile.myWorkflow, { ...args }, { * onComplete: internal.myFile.handleComplete, * context: { ...passed through to onComplete }, * }); * ``` * * @param ctx - The Convex mutation or action context. * @param workflow - The workflow to start (e.g. `internal.myFile.myWorkflow`). * @param args - The workflow arguments. * @param options - Options like `onComplete`, `context`, `startAsync`. * @returns The workflow ID. */ export async function start(ctx, workflow, args, options) { const formatted = { args }; if (options?.onComplete) { formatted.onComplete = await createFunctionHandle(options.onComplete); } if (options?.context !== undefined) { formatted.context = options.context; } if (options?.startAsync !== undefined) { formatted.startAsync = options.startAsync; } return (await ctx.runMutation(workflow, formatted)); } /** * Get a workflow's status. * * @param ctx - The Convex context. * @param component - The workflow component. * @param workflowId - The workflow ID. * @returns The workflow status. */ export async function getStatus(ctx, component, workflowId) { const { workflow, inProgress } = await ctx.runQuery(component.workflow.getStatus, { workflowId }); const running = inProgress.map((entry) => entry.step); switch (workflow.runResult?.kind) { case undefined: return { type: "inProgress", running }; case "canceled": return { type: "canceled" }; case "failed": return { type: "failed", error: workflow.runResult.error }; case "success": return { type: "completed", result: workflow.runResult.returnValue }; } } /** * Cancel a running workflow. * * @param ctx - The Convex context. * @param component - The workflow component. * @param workflowId - The workflow ID. */ export async function cancel(ctx, component, workflowId) { await ctx.runMutation(component.workflow.cancel, { workflowId }); } /** * Restart a previously-failed workflow. * * By default it will retry the handler using the existing history of steps. * To restart from the beginning, pass `{from: 0}`. * To restart from a named step or event: `{from: "myName"}`. * To restart from a function call: `{from: internal.foo.bar}`. * * If the function or name were called multiple times, it will restart from * the last invocation. * * @param ctx - The Convex context. * @param component - The workflow component. * @param workflowId - The workflow ID. * @param options - Options for the retry. * @param options.from - The step to retry from. Can be a step number, * a step name, or the function / workflow `internal.foo.bar`. * Steps from this point onwards will be deleted before restarting. * If not provided, the handler will be re-executed using the existing * history of steps. * @param options.startAsync - If true, the workflow will be enqueued * via the workpool instead of running immediately. */ export async function restart(ctx, component, workflowId, options) { let from; if (options?.from !== undefined) { if (typeof options.from === "number" || typeof options.from === "string") { from = options.from; } else { from = safeFunctionName(options.from); } } await ctx.runMutation(component.workflow.restart, { workflowId, from, startAsync: options?.startAsync, }); } /** * Send an event to a workflow. * * @param ctx - From a mutation, action or workflow step. * @param component - The workflow component. * @param args - Either send an event by its ID, or by name and workflow ID. * If you have a validator, you must provide a value. * If you provide an error string, awaiting the event will throw an error. */ export async function sendEvent(ctx, component, args) { const result = "error" in args ? { kind: "failed", error: args.error } : { kind: "success", returnValue: args.validator ? parse(args.validator, args.value) : "value" in args ? args.value : null, }; return (await ctx.runMutation(component.event.send, { eventId: args.id, result, name: args.name, workflowId: args.workflowId, })); } /** * Create an event ahead of time, enabling awaiting a specific event by ID. * @param ctx - From an action, mutation or workflow step. * @param component - The workflow component. * @param args - The name of the event and what workflow it belongs to. * @returns The event ID, which can be used to send the event or await it. */ export async function createEvent(ctx, component, args) { return (await ctx.runMutation(component.event.create, { name: args.name, workflowId: args.workflowId, })); } /** * List workflows, including their name, args, return value etc. * * @param ctx - The Convex context from a query, mutation, or action. * @param component - The workflow component. * @param opts - How many workflows to fetch and in what order. * e.g. `{ order: "desc", paginationOpts: { cursor: null, numItems: 10 } }` * will get the last 10 workflows in descending order. * Defaults to 100 workflows in ascending order. * @returns The pagination result with per-workflow data. */ export async function list(ctx, component, opts) { const workflows = await ctx.runQuery(component.workflow.list, { order: opts?.order ?? "asc", paginationOpts: opts?.paginationOpts ?? { cursor: null, numItems: 100, }, }); return workflows; } /** * List workflows matching a specific name, including their args, return value etc. * * @param ctx - The Convex context from a query, mutation, or action. * @param component - The workflow component. * @param name - The workflow name to filter by. * @param opts - How many workflows to fetch and in what order. * e.g. `{ order: "desc", paginationOpts: { cursor: null, numItems: 10 } }` * will get the last 10 workflows in descending order. * Defaults to 100 workflows in ascending order. * @returns The pagination result with per-workflow data. */ export async function listByName(ctx, component, name, opts) { const workflows = await ctx.runQuery(component.workflow.listByName, { name, order: opts?.order ?? "asc", paginationOpts: opts?.paginationOpts ?? { cursor: null, numItems: 100, }, }); return workflows; } /** * List the steps in a workflow, including their name, args, return value etc. * * @param ctx - The Convex context from a query, mutation, or action. * @param component - The workflow component. * @param workflowId - The workflow ID. * @param opts - How many steps to fetch and in what order. * e.g. `{ order: "desc", paginationOpts: { cursor: null, numItems: 10 } }` * will get the last 10 steps in descending order. * Defaults to 100 steps in ascending order. * @returns The pagination result with per-step data. */ export async function listSteps(ctx, component, workflowId, opts) { const steps = await ctx.runQuery(component.workflow.listSteps, { workflowId, order: opts?.order ?? "asc", paginationOpts: opts?.paginationOpts ?? { cursor: null, numItems: 100, }, }); return steps; } /** * Clean up a completed workflow's storage. * * @param ctx - The Convex context. * @param component - The workflow component. * @param workflowId - The workflow ID. * @returns - Whether the workflow's state was cleaned up. */ export async function cleanup(ctx, component, workflowId) { return await ctx.runMutation(component.workflow.cleanup, { workflowId, }); } export class WorkflowManager { component; options; constructor(component, options) { this.component = component; this.options = options; } define(workflow) { if (workflow.handler) { return workflowMutation(this.component, workflow, this.options?.workpoolOptions); } // Note: we're passing through more options than defineWorkflow claims // to support, in order to get the maxParallelism / etc. in there. // Direct users of defineWorkflow should instead configure those values // via configuring the component directly. return defineWorkflow(this.component, { ...workflow, workpoolOptions: { ...this.options?.workpoolOptions, ...workflow.workpoolOptions, }, }); } /** * Start a workflow. * * Alternative to `start` (`import { start } from "@convex-dev/workflow"`). * * This is slightly more efficient than calling `start` when passing * `startAsync: true`, and slightly less efficient in the default case. * * @param ctx - The Convex context. * @param workflow - The workflow to start (e.g. `internal.index.exampleWorkflow`). * @param args - The workflow arguments. * @returns The workflow ID. */ async start(ctx, workflow, args, options) { if (!options?.startAsync) { return start(ctx, workflow, args, options); } const handle = await createFunctionHandle(workflow); const onComplete = options?.onComplete ? { fnHandle: await createFunctionHandle(options.onComplete), context: options.context, } : undefined; const workflowId = await ctx.runMutation(this.component.workflow.create, { workflowName: safeFunctionName(workflow), workflowHandle: handle, workflowArgs: args, maxParallelism: this.options?.workpoolOptions?.maxParallelism, onComplete, startAsync: true, }); return workflowId; } /** * Get a workflow's status. * * @param ctx - The Convex context. * @param workflowId - The workflow ID. * @returns The workflow status. */ async status(ctx, workflowId) { return getStatus(ctx, this.component, workflowId); } /** * Restart a previously-failed workflow. * * By default it will retry the handler using the existing history of steps. * To restart from the beginning, pass `{from: 0}`. * To restart from a named step or event: `{from: "myName"}`. * To restart from a function call: `{from: internal.foo.bar}`. * * If the function or name were called multiple times, it will restart from * the last invocation. * * @param ctx - The Convex context. * @param workflowId - The workflow ID. * @param options - Options for the retry. * @param options.from - The step to retry from. Can be a step number, * a step name, or the function / workflow `internal.foo.bar`. * Steps from this point onwards will be deleted before restarting. * If not provided, the handler will be re-executed using the existing * history of steps. * @param options.startAsync - If true, the workflow will be enqueued * via the workpool instead of running immediately. */ async restart(ctx, workflowId, options) { return restart(ctx, this.component, workflowId, options); } /** * Cancel a running workflow. * * @param ctx - The Convex context. * @param workflowId - The workflow ID. */ async cancel(ctx, workflowId) { return cancel(ctx, this.component, workflowId); } /** * List workflows, including their name, args, return value etc. * * @param ctx - The Convex context from a query, mutation, or action. * @param opts - How many workflows to fetch and in what order. * e.g. `{ order: "desc", paginationOpts: { cursor: null, numItems: 10 } }` * will get the last 10 workflows in descending order. * Defaults to 100 workflows in ascending order. * @returns The pagination result with per-workflow data. */ async list(ctx, opts) { return list(ctx, this.component, opts); } /** * List workflows matching a specific name, including their args, return value etc. * * @param ctx - The Convex context from a query, mutation, or action. * @param name - The workflow name to filter by. * @param opts - How many workflows to fetch and in what order. * e.g. `{ order: "desc", paginationOpts: { cursor: null, numItems: 10 } }` * will get the last 10 workflows in descending order. * Defaults to 100 workflows in ascending order. * @returns The pagination result with per-workflow data. */ async listByName(ctx, name, opts) { return listByName(ctx, this.component, name, opts); } /** * List the steps in a workflow, including their name, args, return value etc. * * @param ctx - The Convex context from a query, mutation, or action. * @param workflowId - The workflow ID. * @param opts - How many steps to fetch and in what order. * e.g. `{ order: "desc", paginationOpts: { cursor: null, numItems: 10 } }` * will get the last 10 steps in descending order. * Defaults to 100 steps in ascending order. * @returns The pagination result with per-step data. */ async listSteps(ctx, workflowId, opts) { return listSteps(ctx, this.component, workflowId, opts); } /** * Clean up a completed workflow's storage. * * @param ctx - The Convex context. * @param workflowId - The workflow ID. * @returns - Whether the workflow's state was cleaned up. */ async cleanup(ctx, workflowId) { return cleanup(ctx, this.component, workflowId); } /** * Send an event to a workflow. * * @param ctx - From a mutation, action or workflow step. * @param args - Either send an event by its ID, or by name and workflow ID. * If you have a validator, you must provide a value. * If you provide an error string, awaiting the event will throw an error. */ async sendEvent(ctx, args) { return sendEvent(ctx, this.component, args); } /** * Create an event ahead of time, enabling awaiting a specific event by ID. * @param ctx - From an action, mutation or workflow step. * @param args - The name of the event and what workflow it belongs to. * @returns The event ID, which can be used to send the event or await it. */ async createEvent(ctx, args) { return createEvent(ctx, this.component, args); } } /** * Define an event specification: a name and a validator. * This helps share definitions between workflow.sendEvent and ctx.awaitEvent. * e.g. * ```ts * const approvalEvent = defineEvent({ * name: "approval", * validator: v.object({ approved: v.boolean() }), * }); * ``` * Then you can await it in a workflow: * ```ts * const result = await ctx.awaitEvent(approvalEvent); * ``` * And send from somewhere else: * ```ts * await workflow.sendEvent(ctx, { * ...approvalEvent, * workflowId, * value: { approved: true }, * }); * ``` */ export function defineEvent(spec) { return spec; } //# sourceMappingURL=index.js.map