From bb119ab2fa326802c89a5afbc5e9070f273d5e80 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:43:27 +0000 Subject: [PATCH 01/52] Update actions/checkout action to v5 --- .github/workflows/node.js.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 5cc2680..05c8399 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -8,7 +8,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - name: Use Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - run: npm i From 94a518b88896c58b291df961a54d0563aa17c40e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:43:29 +0000 Subject: [PATCH 02/52] Update actions/setup-node action to v5 --- .github/workflows/node.js.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 5cc2680..86fb51e 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - name: Use Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 - run: npm i - run: npm run build - run: npx pkg-pr-new publish From f1b1446210ed23125ac54acd4ade4395fbf2e240 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Tue, 7 Oct 2025 10:54:07 -0700 Subject: [PATCH 03/52] nested workflow --- example/convex/_generated/api.d.ts | 64 +++++++++- src/client/step.ts | 24 +++- src/client/stepContext.ts | 18 +++ src/client/types.ts | 13 ++ src/component/_generated/api.d.ts | 64 +++++++++- src/component/event.ts | 1 - src/component/journal.ts | 41 ++++-- src/component/pool.ts | 194 ++++++++++++++++------------- src/component/schema.ts | 31 +++-- src/component/workflow.ts | 113 ++++++++++------- 10 files changed, 397 insertions(+), 166 deletions(-) diff --git a/example/convex/_generated/api.d.ts b/example/convex/_generated/api.d.ts index 6651140..ed2644a 100644 --- a/example/convex/_generated/api.d.ts +++ b/example/convex/_generated/api.d.ts @@ -105,6 +105,21 @@ export declare const components: { startedAt: number; workId?: string; } + | { + args: any; + argsSize: number; + completedAt?: number; + handle: string; + inProgress: boolean; + kind: "workflow"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workflowId?: string; + } | { args: { eventId?: string }; argsSize: number; @@ -118,7 +133,6 @@ export declare const components: { | { error: string; kind: "failed" } | { kind: "canceled" }; startedAt: number; - workId?: string; }; stepNumber: number; workflowId: string; @@ -170,6 +184,21 @@ export declare const components: { startedAt: number; workId?: string; } + | { + args: any; + argsSize: number; + completedAt?: number; + handle: string; + inProgress: boolean; + kind: "workflow"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workflowId?: string; + } | { args: { eventId?: string }; argsSize: number; @@ -183,7 +212,6 @@ export declare const components: { | { error: string; kind: "failed" } | { kind: "canceled" }; startedAt: number; - workId?: string; }; }>; workflowId: string; @@ -218,6 +246,21 @@ export declare const components: { startedAt: number; workId?: string; } + | { + args: any; + argsSize: number; + completedAt?: number; + handle: string; + inProgress: boolean; + kind: "workflow"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workflowId?: string; + } | { args: { eventId?: string }; argsSize: number; @@ -231,7 +274,6 @@ export declare const components: { | { error: string; kind: "failed" } | { kind: "canceled" }; startedAt: number; - workId?: string; }; stepNumber: number; workflowId: string; @@ -302,6 +344,21 @@ export declare const components: { startedAt: number; workId?: string; } + | { + args: any; + argsSize: number; + completedAt?: number; + handle: string; + inProgress: boolean; + kind: "workflow"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workflowId?: string; + } | { args: { eventId?: string }; argsSize: number; @@ -315,7 +372,6 @@ export declare const components: { | { error: string; kind: "failed" } | { kind: "canceled" }; startedAt: number; - workId?: string; }; stepNumber: number; workflowId: string; diff --git a/src/client/step.ts b/src/client/step.ts index cc0d150..8634217 100644 --- a/src/client/step.ts +++ b/src/client/step.ts @@ -37,6 +37,11 @@ export type StepRequest = { | { kind: "event"; args: { eventId?: EventId }; + } + | { + kind: "workflow"; + function: FunctionReference<"mutation", "internal">; + args: unknown; }; retry: RetryBehavior | boolean | undefined; schedulerOptions: SchedulerOptions; @@ -163,15 +168,22 @@ export class StepExecutor { target.kind === "function" ? { kind: "function" as const, - ...commonFields, functionType: target.functionType, handle: await createFunctionHandle(target.function), - } - : { - kind: "event" as const, ...commonFields, - args: target.args, - }; + } + : target.kind === "workflow" + ? { + kind: "workflow" as const, + handle: await createFunctionHandle(target.function), + ...commonFields, + } + : { + kind: "event" as const, + eventId: target.args.eventId, + ...commonFields, + args: target.args, + }; return { retry: message.retry, schedulerOptions: message.schedulerOptions, diff --git a/src/client/stepContext.ts b/src/client/stepContext.ts index f155fbe..f3fe4a2 100644 --- a/src/client/stepContext.ts +++ b/src/client/stepContext.ts @@ -42,6 +42,24 @@ export class StepContext implements WorkflowStep { return this.runFunction("action", action, args, opts); } + async runWorkflow>( + workflow: Workflow, + args: FunctionArgs, + opts?: RunOptions, + ): Promise> { + const { name, ...schedulerOptions } = opts ?? {}; + return this.run({ + name: name ?? safeFunctionName(workflow), + target: { + kind: "workflow", + function: workflow, + args, + }, + retry: undefined, + schedulerOptions, + }); + } + async awaitEvent( event: EventSpec, ): Promise { diff --git a/src/client/types.ts b/src/client/types.ts index 1632ff8..b12b093 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -82,6 +82,19 @@ export type WorkflowStep = { opts?: RunOptions & RetryOption, ): Promise>; + /** + * Run a workflow with the given name and arguments. + * + * @param workflow - The workflow to run, like `internal.index.exampleWorkflow`. + * @param args - The arguments to the workflow function. + * @param opts - Options for retrying, scheduling and naming the workflow. + */ + runWorkflow>( + workflow: Workflow, + args: FunctionArgs, + opts?: RunOptions, + ): Promise>; + /** * Blocks until a matching event is sent to this workflow. * diff --git a/src/component/_generated/api.d.ts b/src/component/_generated/api.d.ts index 32f5453..3f7b2f7 100644 --- a/src/component/_generated/api.d.ts +++ b/src/component/_generated/api.d.ts @@ -99,6 +99,21 @@ export type Mounts = { startedAt: number; workId?: string; } + | { + args: any; + argsSize: number; + completedAt?: number; + handle: string; + inProgress: boolean; + kind: "workflow"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workflowId?: string; + } | { args: { eventId?: string }; argsSize: number; @@ -112,7 +127,6 @@ export type Mounts = { | { error: string; kind: "failed" } | { kind: "canceled" }; startedAt: number; - workId?: string; }; stepNumber: number; workflowId: string; @@ -164,6 +178,21 @@ export type Mounts = { startedAt: number; workId?: string; } + | { + args: any; + argsSize: number; + completedAt?: number; + handle: string; + inProgress: boolean; + kind: "workflow"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workflowId?: string; + } | { args: { eventId?: string }; argsSize: number; @@ -177,7 +206,6 @@ export type Mounts = { | { error: string; kind: "failed" } | { kind: "canceled" }; startedAt: number; - workId?: string; }; }>; workflowId: string; @@ -212,6 +240,21 @@ export type Mounts = { startedAt: number; workId?: string; } + | { + args: any; + argsSize: number; + completedAt?: number; + handle: string; + inProgress: boolean; + kind: "workflow"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workflowId?: string; + } | { args: { eventId?: string }; argsSize: number; @@ -225,7 +268,6 @@ export type Mounts = { | { error: string; kind: "failed" } | { kind: "canceled" }; startedAt: number; - workId?: string; }; stepNumber: number; workflowId: string; @@ -296,6 +338,21 @@ export type Mounts = { startedAt: number; workId?: string; } + | { + args: any; + argsSize: number; + completedAt?: number; + handle: string; + inProgress: boolean; + kind: "workflow"; + name: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + workflowId?: string; + } | { args: { eventId?: string }; argsSize: number; @@ -309,7 +366,6 @@ export type Mounts = { | { error: string; kind: "failed" } | { kind: "canceled" }; startedAt: number; - workId?: string; }; stepNumber: number; workflowId: string; diff --git a/src/component/event.ts b/src/component/event.ts index cf5ae72..6f608e8 100644 --- a/src/component/event.ts +++ b/src/component/event.ts @@ -58,7 +58,6 @@ export async function awaitEvent( } assert(entry.step.kind === "event", "Step is not an event"); entry.step.eventId = event._id; - await ctx.db.replace(entry._id, entry); // if there's a name, see if there's one to consume. // if it's there, mark it consumed and swap in the result. return entry; diff --git a/src/component/journal.ts b/src/component/journal.ts index 8175423..ee68c34 100644 --- a/src/component/journal.ts +++ b/src/component/journal.ts @@ -16,11 +16,12 @@ import { workpoolOptions, } from "./pool.js"; import { internal } from "./_generated/api.js"; -import { type FunctionHandle } from "convex/server"; +import { createFunctionHandle, type FunctionHandle } from "convex/server"; import { getDefaultLogger } from "./utils.js"; import { assert } from "convex-helpers"; import { MAX_JOURNAL_SIZE } from "../shared.js"; import { awaitEvent } from "./event.js"; +import { createHandler } from "./workflow.js"; export const load = query({ args: { @@ -111,15 +112,16 @@ export const startSteps = mutation({ const entries = await Promise.all( args.steps.map(async (stepArgs, index) => { - const { step, retry, schedulerOptions } = stepArgs; + const { retry, schedulerOptions } = stepArgs; const stepNumber = stepNumberBase + index; const stepId = await ctx.db.insert("steps", { workflowId: workflow._id, stepNumber, - step, + step: stepArgs.step, }); let entry = await ctx.db.get(stepId); assert(entry, "Step not found"); + const step = entry.step; const { name } = step; if (step.kind === "event") { // Note: This modifies entry in place as well. @@ -127,16 +129,35 @@ export const startSteps = mutation({ name, eventId: step.args.eventId, }); - if (entry.step.runResult) { + if (step.runResult) { console.event("eventConsumed", { workflowId: entry.workflowId, workflowName: workflow.name, - status: entry.step.runResult.kind, - eventName: entry.step.name, - stepNumber: entry.stepNumber, - durationMs: entry.step.completedAt! - entry.step.startedAt, + status: step.runResult.kind, + eventName: step.name, + stepNumber: stepNumber, + durationMs: step.completedAt! - step.startedAt, }); } + } else if (step.kind === "workflow") { + const workflowId = await createHandler(ctx, { + workflowName: step.name, + workflowHandle: step.handle, + workflowArgs: step.args, + maxParallelism: args.workpoolOptions?.maxParallelism, + onComplete: { + fnHandle: await createFunctionHandle( + internal.pool.nestedWorkflowOnComplete, + ), + context: { + stepId, + generationNumber, + workpoolOptions: args.workpoolOptions, + } satisfies OnCompleteContext, + }, + startAsync: true, + }); + step.workflowId = workflowId; } else { const context: OnCompleteContext = { generationNumber, @@ -173,9 +194,9 @@ export const startSteps = mutation({ break; } } - entry.step.workId = workId; - await ctx.db.replace(entry._id, entry); + step.workId = workId; } + await ctx.db.replace(entry._id, entry); console.event("started", { workflowId: workflow._id, diff --git a/src/component/pool.ts b/src/component/pool.ts index 0663fa3..ef63afb 100644 --- a/src/component/pool.ts +++ b/src/component/pool.ts @@ -3,6 +3,8 @@ import { vRetryBehavior, vWorkIdValidator, Workpool, + type RunResult, + type WorkId, type WorkpoolOptions, } from "@convex-dev/workpool"; import { assert } from "convex-helpers"; @@ -20,6 +22,7 @@ import { getWorkflow } from "./model.js"; import { getDefaultLogger } from "./utils.js"; import { completeHandler } from "./workflow.js"; import type { Doc } from "./_generated/dataModel.js"; +import { vWorkflowId, type WorkflowId } from "../types.js"; export const workpoolOptions = v.object({ logLevel: v.optional(logLevel), @@ -70,95 +73,116 @@ export const onComplete = internalMutation({ context: v.any(), // Ensure we can catch invalid context to fail workflow. }, returns: v.null(), - handler: async (ctx, args) => { - const console = await getDefaultLogger(ctx); - const stepId = - "stepId" in args.context - ? ctx.db.normalizeId("steps", args.context.stepId) - : null; - if (!stepId) { - // Write to failures table and return - // So someone can investigate if this ever happens - console.error("Invalid onComplete context", args.context); - await ctx.db.insert("onCompleteFailures", args); - return; - } - const journalEntry = await ctx.db.get(stepId); - assert(journalEntry, `Journal entry not found: ${stepId}`); - const workflowId = journalEntry.workflowId; + handler: onCompleteHandler, +}); - if ( - !validate(onCompleteContext, args.context, { allowUnknownFields: true }) - ) { - const error = - `Invalid onComplete context for workId ${args.workId}` + - JSON.stringify(args.context); - await ctx.db.patch(workflowId, { - runResult: { - kind: "failed", - error, - }, - }); - return; - } - const { generationNumber } = args.context; - const workflow = await getWorkflow(ctx, workflowId, null); - if (workflow.generationNumber !== generationNumber) { - console.error( - `Workflow: ${workflowId} already has generation number ${workflow.generationNumber} when completing ${stepId}`, - ); - return; - } - if (!journalEntry.step.inProgress) { - console.error( - `Step finished but journal entry not in progress: ${stepId} status: ${journalEntry.step.runResult?.kind ?? "pending"}`, - ); - return; - } - journalEntry.step.inProgress = false; - journalEntry.step.completedAt = Date.now(); - switch (args.result.kind) { - case "success": - journalEntry.step.runResult = { - kind: "success", - returnValue: args.result.returnValue, - }; - break; - case "failed": - journalEntry.step.runResult = { - kind: "failed", - error: args.result.error, - }; - break; - case "canceled": - journalEntry.step.runResult = { - kind: "canceled", - }; - break; - } - await ctx.db.replace(journalEntry._id, journalEntry); - console.debug(`Completed execution of ${stepId}`, journalEntry); +// For a nested workflow +export const nestedWorkflowOnComplete = internalMutation({ + args: { + workflowId: vWorkflowId, + result: vResultValidator, + context: v.any(), + }, + returns: v.null(), + handler: onCompleteHandler, +}); - console.event("stepCompleted", { - workflowId, - workflowName: workflow.name, - status: args.result.kind, - stepName: journalEntry.step.name, - stepNumber: journalEntry.stepNumber, - durationMs: journalEntry.step.completedAt - journalEntry.step.startedAt, +async function onCompleteHandler( + ctx: MutationCtx, + args: { + workId?: WorkId; + workflowId?: WorkflowId; + result: RunResult; + context: object; + }, +) { + const console = await getDefaultLogger(ctx); + const stepId = + "stepId" in args.context && typeof args.context.stepId === "string" + ? ctx.db.normalizeId("steps", args.context.stepId) + : null; + if (!stepId) { + // Write to failures table and return + // So someone can investigate if this ever happens + console.error("Invalid onComplete context", args.context); + await ctx.db.insert("onCompleteFailures", args); + return; + } + const journalEntry = await ctx.db.get(stepId); + assert(journalEntry, `Journal entry not found: ${stepId}`); + const workflowId = journalEntry.workflowId; + + if ( + !validate(onCompleteContext, args.context, { allowUnknownFields: true }) + ) { + const error = + `Invalid onComplete context for ${args.workId ? `workId ${args.workId}` : `nested workflowId ${args.workflowId}`}` + + JSON.stringify(args.context); + await ctx.db.patch(workflowId, { + runResult: { + kind: "failed", + error, + }, }); - if (workflow.runResult !== undefined) { - if (workflow.runResult.kind !== "canceled") { - console.error( - `Workflow: ${workflowId} already ${workflow.runResult.kind} when completing ${stepId} with status ${args.result.kind}`, - ); - } - return; + return; + } + const { generationNumber } = args.context; + const workflow = await getWorkflow(ctx, workflowId, null); + if (workflow.generationNumber !== generationNumber) { + console.error( + `Workflow: ${workflowId} already has generation number ${workflow.generationNumber} when completing ${stepId}`, + ); + return; + } + if (!journalEntry.step.inProgress) { + console.error( + `Step finished but journal entry not in progress: ${stepId} status: ${journalEntry.step.runResult?.kind ?? "pending"}`, + ); + return; + } + journalEntry.step.inProgress = false; + journalEntry.step.completedAt = Date.now(); + switch (args.result.kind) { + case "success": + journalEntry.step.runResult = { + kind: "success", + returnValue: args.result.returnValue, + }; + break; + case "failed": + journalEntry.step.runResult = { + kind: "failed", + error: args.result.error, + }; + break; + case "canceled": + journalEntry.step.runResult = { + kind: "canceled", + }; + break; + } + await ctx.db.replace(journalEntry._id, journalEntry); + console.debug(`Completed execution of ${stepId}`, journalEntry); + + console.event("stepCompleted", { + workflowId, + workflowName: workflow.name, + status: args.result.kind, + stepName: journalEntry.step.name, + stepNumber: journalEntry.stepNumber, + durationMs: journalEntry.step.completedAt - journalEntry.step.startedAt, + }); + if (workflow.runResult !== undefined) { + if (workflow.runResult.kind !== "canceled") { + console.error( + `Workflow: ${workflowId} already ${workflow.runResult.kind} when completing ${stepId} with status ${args.result.kind}`, + ); } - const workpool = await getWorkpool(ctx, args.context.workpoolOptions); - await enqueueWorkflow(ctx, workflow, workpool); - }, -}); + return; + } + const workpool = await getWorkpool(ctx, args.context.workpoolOptions); + await enqueueWorkflow(ctx, workflow, workpool); +} export async function enqueueWorkflow( ctx: MutationCtx, diff --git a/src/component/schema.ts b/src/component/schema.ts index 749afaf..0cfdd3c 100644 --- a/src/component/schema.ts +++ b/src/component/schema.ts @@ -62,7 +62,6 @@ export type Workflow = Infer; const stepCommonFields = { name: v.string(), inProgress: v.boolean(), - workId: v.optional(vWorkIdValidator), argsSize: v.number(), args: v.any(), runResult: v.optional(vResultValidator), @@ -75,6 +74,13 @@ export const step = v.union( kind: v.optional(v.literal("function")), functionType: literals("query", "mutation", "action"), handle: v.string(), + workId: v.optional(vWorkIdValidator), + ...stepCommonFields, + }), + v.object({ + kind: v.literal("workflow"), + handle: v.string(), + workflowId: v.optional(v.id("workflows")), ...stepCommonFields, }), v.object({ @@ -90,13 +96,21 @@ function stepSize(step: Step): number { let size = 0; size += step.name.length; size += 1; // inProgress - if (step.workId) { - size += step.workId.length; - } if (step.kind) size += step.kind.length; - if (step.kind !== "event") { - size += step.functionType.length; - size += step.handle.length; + switch (step.kind) { + case undefined: + case "function": + size += step.handle.length; + size += step.functionType.length; + size += step.workId?.length ?? 0; + break; + case "workflow": + size += step.handle.length; + size += step.workflowId?.length ?? 0; + break; + case "event": + size += step.eventId?.length ?? 0; + break; } size += 8 + step.argsSize; if (step.runResult) { @@ -173,7 +187,8 @@ export default defineSchema({ onCompleteFailures: defineTable( v.union( v.object({ - workId: vWorkIdValidator, + workId: v.optional(vWorkIdValidator), + workflowId: v.optional(v.string()), result: vResultValidator, context: v.any(), }), diff --git a/src/component/workflow.ts b/src/component/workflow.ts index fe03b06..40c47a0 100644 --- a/src/component/workflow.ts +++ b/src/component/workflow.ts @@ -9,57 +9,66 @@ import { getWorkpool } from "./pool.js"; import { journalDocument, vOnComplete, workflowDocument } from "./schema.js"; import { getDefaultLogger } from "./utils.js"; import type { WorkflowId, OnCompleteArgs } from "../types.js"; -import { internal } from "./_generated/api.js"; +import { api, internal } from "./_generated/api.js"; import { formatErrorWithStack } from "../shared.js"; +import type { SchedulerOptions } from "../client/types.js"; +const createArgs = v.object({ + workflowName: v.string(), + workflowHandle: v.string(), + workflowArgs: v.any(), + maxParallelism: v.optional(v.number()), + onComplete: v.optional(vOnComplete), + startAsync: v.optional(v.boolean()), + // TODO: ttl +}); export const create = mutation({ - args: { - workflowName: v.string(), - workflowHandle: v.string(), - workflowArgs: v.any(), - maxParallelism: v.optional(v.number()), - onComplete: v.optional(vOnComplete), - startAsync: v.optional(v.boolean()), - // TODO: ttl - }, + args: createArgs, returns: v.id("workflows"), - handler: async (ctx, args) => { - const console = await getDefaultLogger(ctx); - await updateMaxParallelism(ctx, console, args.maxParallelism); - const workflowId = await ctx.db.insert("workflows", { - name: args.workflowName, - workflowHandle: args.workflowHandle, - args: args.workflowArgs, + handler: createHandler, +}); + +export async function createHandler( + ctx: MutationCtx, + args: Infer, + schedulerOptions?: SchedulerOptions, +) { + const console = await getDefaultLogger(ctx); + await updateMaxParallelism(ctx, console, args.maxParallelism); + const workflowId = await ctx.db.insert("workflows", { + name: args.workflowName, + workflowHandle: args.workflowHandle, + args: args.workflowArgs, + generationNumber: 0, + onComplete: args.onComplete, + }); + console.debug( + `Created workflow ${workflowId}:`, + args.workflowArgs, + args.workflowHandle, + ); + if (args.startAsync) { + const workpool = await getWorkpool(ctx, args); + await workpool.enqueueMutation( + ctx, + args.workflowHandle as FunctionHandle<"mutation">, + { workflowId, generationNumber: 0 }, + { + name: args.workflowName, + onComplete: internal.pool.handlerOnComplete, + context: { workflowId, generationNumber: 0 }, + ...schedulerOptions, + }, + ); + } else { + // If we can't start it, may as well not create it, eh? Fail fast... + await ctx.runMutation(args.workflowHandle as FunctionHandle<"mutation">, { + workflowId, generationNumber: 0, - onComplete: args.onComplete, }); - console.debug( - `Created workflow ${workflowId}:`, - args.workflowArgs, - args.workflowHandle, - ); - if (args.startAsync) { - const workpool = await getWorkpool(ctx, args); - await workpool.enqueueMutation( - ctx, - args.workflowHandle as FunctionHandle<"mutation">, - { workflowId, generationNumber: 0 }, - { - name: args.workflowName, - onComplete: internal.pool.handlerOnComplete, - context: { workflowId, generationNumber: 0 }, - }, - ); - } else { - // If we can't start it, may as well not create it, eh? Fail fast... - await ctx.runMutation(args.workflowHandle as FunctionHandle<"mutation">, { - workflowId, - generationNumber: 0, - }); - } - return workflowId; - }, -}); + } + return workflowId; +} export const getStatus = query({ args: { @@ -147,9 +156,17 @@ export async function completeHandler( .collect(); if (inProgress.length > 0) { const workpool = await getWorkpool(ctx, {}); - for (const step of inProgress) { - if (step.step.workId) { - await workpool.cancel(ctx, step.step.workId); + for (const { step } of inProgress) { + if (!step.kind || step.kind === "function") { + if (step.workId) { + await workpool.cancel(ctx, step.workId); + } + } else if (step.kind === "workflow") { + if (step.workflowId) { + await ctx.runMutation(api.workflow.cancel, { + workflowId: step.workflowId, + }); + } } } } From 7bbf9f62deb280438039d8d01f47a099ce2caa15 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Tue, 7 Oct 2025 11:23:04 -0700 Subject: [PATCH 04/52] lint --- example/convex/userConfirmation.ts | 2 +- src/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/convex/userConfirmation.ts b/example/convex/userConfirmation.ts index 3ba95b9..e3c4c1f 100644 --- a/example/convex/userConfirmation.ts +++ b/example/convex/userConfirmation.ts @@ -32,7 +32,7 @@ export const confirmationWorkflow = workflow.define({ export const generateProposals = internalAction({ args: { prompt: v.string() }, - handler: async (ctx, args) => { + handler: async (_ctx, _args) => { // imagine this is a call to an LLM return ["proposal1", "proposal2", "proposal3"]; }, diff --git a/src/types.ts b/src/types.ts index 86d588d..e8d3895 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ import type { RunResult } from "@convex-dev/workpool"; -import { v, type Validator, type VNull, type VString } from "convex/values"; +import { v, type Validator, type VString } from "convex/values"; export type WorkflowId = string & { __isWorkflowId: true }; export const vWorkflowId = v.string() as VString; From 3a0f282e69763466c071e63c070026ddb84d3649 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:24:32 +0000 Subject: [PATCH 05/52] Pin dependencies (#136) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 16 ++++++++++++++-- package.json | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46d17c3..8d3bbf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "async-channel": "^0.2.0" }, "devDependencies": { - "@convex-dev/workpool": "^0.2.19-alpha.2", + "@convex-dev/workpool": "0.2.19-alpha.2", "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", @@ -20,7 +20,7 @@ "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", - "convex": "^1.27.3", + "convex": "1.27.4", "convex-test": "0.0.38", "cpy-cli": "6.0.0", "eslint": "9.37.0", @@ -118,6 +118,7 @@ "integrity": "sha512-NKBGBSIKUG584qrS1tyxVpX/AKJKQw5HgjYEnPLC0QsTw79JrGn+qUr8CXFb955Iy7GUdiiUv1rJ6JBGvaKb6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@edge-runtime/primitives": "6.0.0" }, @@ -878,6 +879,7 @@ "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -1415,6 +1417,7 @@ "integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1465,6 +1468,7 @@ "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.40.0", "@typescript-eslint/types": "8.40.0", @@ -1798,6 +1802,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2135,6 +2140,7 @@ "resolved": "https://registry.npmjs.org/convex/-/convex-1.27.4.tgz", "integrity": "sha512-aPP3uxOF5v+K4uftXxRh8GAYepsjsFgU+S9IpAyLVNaFU3Z72WB1rIhaSzPAo4Q0TJWsOKANFGU903IU92QDTA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" @@ -2413,6 +2419,7 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4022,6 +4029,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4122,6 +4130,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4233,6 +4242,7 @@ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -4346,6 +4356,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4681,6 +4692,7 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "devOptional": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index f1475b1..4eb891c 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "async-channel": "^0.2.0" }, "devDependencies": { - "@convex-dev/workpool": "^0.2.19-alpha.2", + "@convex-dev/workpool": "0.2.19-alpha.2", "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", @@ -67,7 +67,7 @@ "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", - "convex": "^1.27.3", + "convex": "1.27.4", "convex-test": "0.0.38", "cpy-cli": "6.0.0", "eslint": "9.37.0", From d032c8fd654003384e96e0c5ad45eb7a59d019e4 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Tue, 7 Oct 2025 11:27:05 -0700 Subject: [PATCH 06/52] 0.2.8-alpha.0 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a9c558..4c18286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.2.8 alpha + +- Adds asynchronous events - wait for an event in a workflow, send + events asynchronously - allows pause/resume, human-in-loop, etc. +- Supports nested workflows with step.runWorkflow. +- Reduces read bandwidth when reading the journal after running many steps in parallel. + ## 0.2.7 - Support for console logging & timing in workflows diff --git a/package-lock.json b/package-lock.json index 46d17c3..865809e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@convex-dev/workflow", - "version": "0.2.7", + "version": "0.2.8-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@convex-dev/workflow", - "version": "0.2.7", + "version": "0.2.8-alpha.0", "license": "Apache-2.0", "dependencies": { "async-channel": "^0.2.0" diff --git a/package.json b/package.json index f1475b1..02b6b0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@convex-dev/workflow", - "version": "0.2.7", + "version": "0.2.8-alpha.0", "description": "Convex component for durably executing workflows.", "keywords": [ "convex", From 10d5ce22540872bf73a1a4186d56868da804af41 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Tue, 7 Oct 2025 11:33:18 -0700 Subject: [PATCH 07/52] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c18286..90e2f9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ - Adds asynchronous events - wait for an event in a workflow, send events asynchronously - allows pause/resume, human-in-loop, etc. - Supports nested workflows with step.runWorkflow. +- You can start a workflow directly from the CLI / dashboard without having to + make a mutation to call workflow.start: + - `{ fn: "path/to/file:workflowName", args: { ...your workflow args } }` - Reduces read bandwidth when reading the journal after running many steps in parallel. ## 0.2.7 From 33c1d06c122b3403e72659242f60a32fe9c0fde4 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Tue, 7 Oct 2025 12:22:22 -0700 Subject: [PATCH 08/52] surface return result in completed status --- src/client/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/index.ts b/src/client/index.ts index 9aac212..1465baf 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -76,7 +76,7 @@ export type WorkflowDefinition< export type WorkflowStatus = | { type: "inProgress"; running: OpaqueIds[] } - | { type: "completed" } + | { type: "completed"; result: unknown } | { type: "canceled" } | { type: "failed"; error: string }; @@ -185,7 +185,7 @@ export class WorkflowManager { case "failed": return { type: "failed", error: workflow.runResult.error }; case "success": - return { type: "completed" }; + return { type: "completed", result: workflow.runResult.returnValue }; } } From 3029210a7f46bd8ccfb378fdd8317a85a4c55715 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Tue, 7 Oct 2025 12:25:57 -0700 Subject: [PATCH 09/52] 0.2.8-alpha.1 --- CHANGELOG.md | 1 + package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90e2f9f..f1bfac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Adds asynchronous events - wait for an event in a workflow, send events asynchronously - allows pause/resume, human-in-loop, etc. - Supports nested workflows with step.runWorkflow. +- Surfaces return value of the workflow in the status - You can start a workflow directly from the CLI / dashboard without having to make a mutation to call workflow.start: - `{ fn: "path/to/file:workflowName", args: { ...your workflow args } }` diff --git a/package-lock.json b/package-lock.json index 865809e..855ffdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.0", + "version": "0.2.8-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.0", + "version": "0.2.8-alpha.1", "license": "Apache-2.0", "dependencies": { "async-channel": "^0.2.0" diff --git a/package.json b/package.json index 02b6b0b..2dfd8ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.0", + "version": "0.2.8-alpha.1", "description": "Convex component for durably executing workflows.", "keywords": [ "convex", From a1ba3763eed37163578060abeaaa35c37f0d178f Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Tue, 7 Oct 2025 17:43:31 -0700 Subject: [PATCH 10/52] list steps --- example/convex/_generated/api.d.ts | 39 ++++++++++++++ src/client/index.ts | 44 ++++++++++++++-- src/client/step.ts | 2 +- src/client/stepContext.ts | 4 +- src/client/types.ts | 6 +-- src/component/_generated/api.d.ts | 40 ++++++++++++++ src/component/workflow.ts | 78 +++++++++++++++++++++++++-- src/types.ts | 85 ++++++++++++++++++++++++++++-- 8 files changed, 283 insertions(+), 15 deletions(-) diff --git a/example/convex/_generated/api.d.ts b/example/convex/_generated/api.d.ts index ed2644a..45431cc 100644 --- a/example/convex/_generated/api.d.ts +++ b/example/convex/_generated/api.d.ts @@ -395,6 +395,45 @@ export declare const components: { }; } >; + listSteps: FunctionReference< + "query", + "internal", + { + order: "asc" | "desc"; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + workflowId: string; + }, + { + continueCursor: string; + isDone: boolean; + page: Array<{ + args: any; + completedAt?: number; + eventId?: string; + kind: "function" | "workflow" | "event"; + name: string; + nestedWorkflowId?: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + stepId: string; + stepNumber: number; + workId?: string; + workflowId: string; + }>; + pageStatus?: "SplitRecommended" | "SplitRequired" | null; + splitCursor?: string | null; + } + >; }; }; }; diff --git a/src/client/index.ts b/src/client/index.ts index 1465baf..f41e4c4 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -10,6 +10,8 @@ import { type GenericDataModel, type GenericMutationCtx, type GenericQueryCtx, + type PaginationOptions, + type PaginationResult, type RegisteredMutation, type ReturnValueForOptionalValidator, } from "convex/server"; @@ -20,13 +22,19 @@ import type { EventSpec, OnCompleteArgs, WorkflowId, + WorkflowStep, } from "../types.js"; import { safeFunctionName } from "./safeFunctionName.js"; -import type { OpaqueIds, WorkflowComponent, WorkflowStep } from "./types.js"; +import type { OpaqueIds, WorkflowComponent, WorkflowCtx } from "./types.js"; import { workflowMutation } from "./workflowMutation.js"; import { parse } from "convex-helpers/validators"; -export { vWorkflowId, type WorkflowId } from "../types.js"; +export { + vWorkflowId, + type WorkflowId, + vWorkflowStep, + type WorkflowStep, +} from "../types.js"; export type { RunOptions } from "./types.js"; export { defineEvent } from "./events.js"; @@ -67,7 +75,7 @@ export type WorkflowDefinition< > = { args?: ArgsValidator; handler: ( - step: WorkflowStep, + step: WorkflowCtx, args: ObjectType, ) => Promise>; returns?: ReturnsValidator; @@ -201,6 +209,36 @@ export class WorkflowManager { }); } + /** + * 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: RunQueryCtx, + workflowId: WorkflowId, + opts?: { + order?: "asc" | "desc"; + paginationOpts?: PaginationOptions; + }, + ): Promise> { + const steps = await ctx.runQuery(this.component.workflow.listSteps, { + workflowId, + order: opts?.order ?? "asc", + paginationOpts: opts?.paginationOpts ?? { + cursor: null, + numItems: 100, + }, + }); + return steps as PaginationResult; + } + /** * Clean up a completed workflow's storage. * diff --git a/src/client/step.ts b/src/client/step.ts index 8634217..466bf12 100644 --- a/src/client/step.ts +++ b/src/client/step.ts @@ -36,7 +36,7 @@ export type StepRequest = { } | { kind: "event"; - args: { eventId?: EventId }; + args: { eventId?: EventId }; } | { kind: "workflow"; diff --git a/src/client/stepContext.ts b/src/client/stepContext.ts index f3fe4a2..867f6c7 100644 --- a/src/client/stepContext.ts +++ b/src/client/stepContext.ts @@ -8,11 +8,11 @@ import type { import { safeFunctionName } from "./safeFunctionName.js"; import type { StepRequest } from "./step.js"; import type { RetryOption } from "@convex-dev/workpool"; -import type { RunOptions, WorkflowStep } from "./types.js"; +import type { RunOptions, WorkflowCtx } from "./types.js"; import type { EventSpec, WorkflowId } from "../types.js"; import { parse } from "convex-helpers/validators"; -export class StepContext implements WorkflowStep { +export class StepContext implements WorkflowCtx { constructor( public workflowId: WorkflowId, private sender: BaseChannel, diff --git a/src/client/types.ts b/src/client/types.ts index b12b093..e8ade97 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -7,7 +7,7 @@ import type { } from "convex/server"; import type { api } from "../component/_generated/api.js"; import type { GenericId } from "convex/values"; -import type { EventSpec, WorkflowId } from "../types.js"; +import type { EventId, EventSpec, WorkflowId } from "../types.js"; export type WorkflowComponent = UseApi; @@ -38,7 +38,7 @@ export type SchedulerOptions = runAfter?: number; }; -export type WorkflowStep = { +export type WorkflowCtx = { /** * The ID of the workflow currently running. */ @@ -132,7 +132,7 @@ export type UseApi = Expand<{ export type OpaqueIds = T extends GenericId ? string - : T extends WorkId + : T extends WorkId | WorkflowId | EventId ? string : T extends (infer U)[] ? OpaqueIds[] diff --git a/src/component/_generated/api.d.ts b/src/component/_generated/api.d.ts index 3f7b2f7..af0b54d 100644 --- a/src/component/_generated/api.d.ts +++ b/src/component/_generated/api.d.ts @@ -389,6 +389,45 @@ export type Mounts = { }; } >; + listSteps: FunctionReference< + "query", + "public", + { + order: "asc" | "desc"; + paginationOpts: { + cursor: string | null; + endCursor?: string | null; + id?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; + numItems: number; + }; + workflowId: string; + }, + { + continueCursor: string; + isDone: boolean; + page: Array<{ + args: any; + completedAt?: number; + eventId?: string; + kind: "function" | "workflow" | "event"; + name: string; + nestedWorkflowId?: string; + runResult?: + | { kind: "success"; returnValue: any } + | { error: string; kind: "failed" } + | { kind: "canceled" }; + startedAt: number; + stepId: string; + stepNumber: number; + workId?: string; + workflowId: string; + }>; + pageStatus?: "SplitRecommended" | "SplitRequired" | null; + splitCursor?: string | null; + } + >; }; }; // For now fullApiWithMounts is only fullApi which provides @@ -422,6 +461,7 @@ export declare const components: { "internal", { before?: number; + limit?: number; logLevel: "DEBUG" | "TRACE" | "INFO" | "REPORT" | "WARN" | "ERROR"; }, any diff --git a/src/component/workflow.ts b/src/component/workflow.ts index 40c47a0..67284d3 100644 --- a/src/component/workflow.ts +++ b/src/component/workflow.ts @@ -1,17 +1,35 @@ import { vResultValidator } from "@convex-dev/workpool"; import { assert } from "convex-helpers"; -import type { FunctionHandle } from "convex/server"; +import { + paginationOptsValidator, + type FunctionHandle, + type PaginationResult, +} from "convex/server"; import { type Infer, v } from "convex/values"; import { mutation, type MutationCtx, query } from "./_generated/server.js"; import { type Logger, logLevel } from "./logging.js"; import { getWorkflow } from "./model.js"; import { getWorkpool } from "./pool.js"; -import { journalDocument, vOnComplete, workflowDocument } from "./schema.js"; +import schema, { + journalDocument, + vOnComplete, + workflowDocument, + type JournalEntry, +} from "./schema.js"; import { getDefaultLogger } from "./utils.js"; -import type { WorkflowId, OnCompleteArgs } from "../types.js"; +import { + type WorkflowId, + type OnCompleteArgs, + type WorkflowStep, + type EventId, + vPaginationResult, + vWorkflowStep, +} from "../types.js"; import { api, internal } from "./_generated/api.js"; import { formatErrorWithStack } from "../shared.js"; import type { SchedulerOptions } from "../client/types.js"; +import type { Id } from "./_generated/dataModel.js"; +import { paginator } from "convex-helpers/server/pagination"; const createArgs = v.object({ workflowName: v.string(), @@ -95,6 +113,60 @@ export const getStatus = query({ }, }); +function publicWorkflowId(workflowId: Id<"workflows">): WorkflowId { + return workflowId as any; +} + +function publicStep(step: JournalEntry): WorkflowStep { + return { + workflowId: publicWorkflowId(step.workflowId), + name: step.step.name, + stepId: step._id, + stepNumber: step.stepNumber, + + args: step.step.args, + runResult: step.step.runResult, + + startedAt: step.step.startedAt, + completedAt: step.step.completedAt, + + ...(step.step.kind === "event" + ? { + kind: "event", + eventId: step.step.eventId as unknown as EventId, + } + : step.step.kind === "workflow" + ? { + kind: "workflow", + nestedWorkflowId: publicWorkflowId(step.step.workflowId!), + } + : { + kind: "function", + workId: step.step.workId!, + }), + } satisfies WorkflowStep; +} + +export const listSteps = query({ + args: { + workflowId: v.id("workflows"), + order: v.union(v.literal("asc"), v.literal("desc")), + paginationOpts: paginationOptsValidator, + }, + returns: vPaginationResult(vWorkflowStep), + handler: async (ctx, args) => { + const result = await paginator(ctx.db, schema) + .query("steps") + .withIndex("workflow", (q) => q.eq("workflowId", args.workflowId)) + .order(args.order) + .paginate(args.paginationOpts); + return { + ...result, + page: result.page.map(publicStep), + } as PaginationResult>; + }, +}); + export const cancel = mutation({ args: { workflowId: v.id("workflows"), diff --git a/src/types.ts b/src/types.ts index e8d3895..cbca318 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,21 @@ -import type { RunResult } from "@convex-dev/workpool"; -import { v, type Validator, type VString } from "convex/values"; +import { + vResultValidator, + vWorkIdValidator, + type RunResult, + type WorkId, +} from "@convex-dev/workpool"; +import { + v, + type Infer, + type Validator, + type Value, + type VString, +} from "convex/values"; export type WorkflowId = string & { __isWorkflowId: true }; export const vWorkflowId = v.string() as VString; -export type EventId = string & { +export type EventId = string & { __isEventId: true; __name: Name; }; @@ -17,6 +28,56 @@ export type EventSpec = { id?: EventId; }; +export type WorkflowStep = { + workflowId: WorkflowId; + name: string; + stepId: string; + stepNumber: number; + + args: unknown; + runResult?: RunResult; + + startedAt: number; + completedAt?: number; +} & ( + | { + kind: "function"; + workId: WorkId; + } + | { + kind: "workflow"; + nestedWorkflowId: WorkflowId; + } + | { + kind: "event"; + eventId: EventId; + } +); + +export const vWorkflowStep = v.object({ + workflowId: vWorkflowId, + name: v.string(), + stepId: v.string(), + stepNumber: v.number(), + + args: v.any(), + runResult: v.optional(vResultValidator), + + startedAt: v.number(), + completedAt: v.optional(v.number()), + + kind: v.union( + v.literal("function"), + v.literal("workflow"), + v.literal("event"), + ), + workId: v.optional(vWorkIdValidator), + nestedWorkflowId: v.optional(vWorkflowId), + eventId: v.optional(vEventId), +}); +// type assertion to keep us in check +const _: Infer = {} as WorkflowStep; + export type OnCompleteArgs = { /** * The ID of the work that completed. @@ -32,3 +93,21 @@ export type OnCompleteArgs = { */ result: RunResult; }; + +export function vPaginationResult< + T extends Validator, +>(itemValidator: T) { + return v.object({ + page: v.array(itemValidator), + continueCursor: v.string(), + isDone: v.boolean(), + splitCursor: v.optional(v.union(v.string(), v.null())), + pageStatus: v.optional( + v.union( + v.literal("SplitRecommended"), + v.literal("SplitRequired"), + v.null(), + ), + ), + }); +} From 781fa7d851e06f6cd5752e9533e210f2cb9611ea Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Wed, 8 Oct 2025 12:04:48 -0700 Subject: [PATCH 11/52] onComplete only requires a string workflowId --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index cbca318..b1614e2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -82,7 +82,7 @@ export type OnCompleteArgs = { /** * The ID of the work that completed. */ - workflowId: WorkflowId; + workflowId: string; /** * The context object passed when enqueuing the work. * Useful for passing data from the enqueue site to the onComplete site. From 27c2eeae4e8120203a1a8dd1a91bd4c45365dea0 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Wed, 8 Oct 2025 12:05:39 -0700 Subject: [PATCH 12/52] 0.2.8-alpha.2 --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1bfac2..5c52d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ make a mutation to call workflow.start: - `{ fn: "path/to/file:workflowName", args: { ...your workflow args } }` - Reduces read bandwidth when reading the journal after running many steps in parallel. +- Simplifies the onComplete type requirement so you can accept a workflowId as a string. + This helps when you have statically generated types which can't do branded strings. ## 0.2.7 diff --git a/package-lock.json b/package-lock.json index 855ffdc..4c10f6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.1", + "version": "0.2.8-alpha.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.1", + "version": "0.2.8-alpha.2", "license": "Apache-2.0", "dependencies": { "async-channel": "^0.2.0" diff --git a/package.json b/package.json index 2dfd8ef..ef5c3ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.1", + "version": "0.2.8-alpha.2", "description": "Convex component for durably executing workflows.", "keywords": [ "convex", From 8e02416579111538ae86fd5afea338d809d4c166 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Wed, 8 Oct 2025 17:32:55 -0700 Subject: [PATCH 13/52] 0.2.8-alpha.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c10f6a..8ba2538 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.2", + "version": "0.2.8-alpha.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.2", + "version": "0.2.8-alpha.3", "license": "Apache-2.0", "dependencies": { "async-channel": "^0.2.0" diff --git a/package.json b/package.json index ef5c3ee..dc77d74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.2", + "version": "0.2.8-alpha.3", "description": "Convex component for durably executing workflows.", "keywords": [ "convex", From 61eaa7766b65d30451b3afad29ce257b197195f9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 22:09:37 +0000 Subject: [PATCH 14/52] Update dependency @types/node to v22.18.9 (#139) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8d3bbf0..f1b14a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", - "@types/node": "22.18.8", + "@types/node": "22.18.9", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", @@ -1412,9 +1412,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.18.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.8.tgz", - "integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==", + "version": "22.18.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.9.tgz", + "integrity": "sha512-5yBtK0k/q8PjkMXbTfeIEP/XVYnz1R9qZJ3yUicdEW7ppdDJfe+MqXEhpqDL3mtn4Wvs1u0KLEG0RXzCgNpsSg==", "dev": true, "license": "MIT", "peer": true, diff --git a/package.json b/package.json index 4eb891c..32ecd06 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", - "@types/node": "22.18.8", + "@types/node": "22.18.9", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", From f3d049681b698d6f84b59742f90b09baaf26c6cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 23:57:11 +0000 Subject: [PATCH 15/52] Update dependency convex to v1.27.5 (#140) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f1b14a2..3e16db4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", - "convex": "1.27.4", + "convex": "1.27.5", "convex-test": "0.0.38", "cpy-cli": "6.0.0", "eslint": "9.37.0", @@ -2136,9 +2136,9 @@ "license": "MIT" }, "node_modules/convex": { - "version": "1.27.4", - "resolved": "https://registry.npmjs.org/convex/-/convex-1.27.4.tgz", - "integrity": "sha512-aPP3uxOF5v+K4uftXxRh8GAYepsjsFgU+S9IpAyLVNaFU3Z72WB1rIhaSzPAo4Q0TJWsOKANFGU903IU92QDTA==", + "version": "1.27.5", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.27.5.tgz", + "integrity": "sha512-6YU/AVPnoNdAaJABKBI9c5IqRSKsow/c4yo/ntaOWtd8Dff2P2zaImA/ougICfPgTuTvjKRbgkxk6lJhODzb4g==", "license": "Apache-2.0", "peer": true, "dependencies": { diff --git a/package.json b/package.json index 32ecd06..170508d 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", - "convex": "1.27.4", + "convex": "1.27.5", "convex-test": "0.0.38", "cpy-cli": "6.0.0", "eslint": "9.37.0", From ee5e919e1c17e46969a2f59da37e041fba56ee8d Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 9 Oct 2025 18:12:04 -0700 Subject: [PATCH 16/52] package script workflow --- CONTRIBUTING.md | 3 ++- package.json | 28 +++++++++++++++------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ced4222..d1fd47d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ ## Running locally ```sh -npm run setup +npm i npm run dev ``` @@ -11,6 +11,7 @@ npm run dev ```sh npm run clean +npm run build npm run typecheck npm run lint npm run test diff --git a/package.json b/package.json index dc77d74..4551865 100644 --- a/package.json +++ b/package.json @@ -13,24 +13,26 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "example": "convex dev --typecheck-components --live-component-sources", - "dev": "run-p -r 'example' 'build:watch'", - "dashboard": "cd example && npx convex dashboard", - "all": "run-p -r 'example' 'build:watch' 'test:watch'", - "setup": "npm i && npm run build && npx convex dev --once", - "build:watch": "cd src && npx chokidar -d 1000 '../tsconfig.json' '**/*.ts' -c 'npm run build' --initial", - "build": "tsc --project ./tsconfig.build.json && npm run copy:dts && echo '{\\n \"type\": \"module\"\\n}' > dist/package.json", + "dev": "run-p -r 'dev:backend' 'dev:frontend' 'build:watch'", + "dev:backend": "convex dev --live-component-sources --typecheck-components", + "dev:frontend": "cd example && vite --clearScreen false", + "predev": "npm run dev:backend -- --until-success", + "clean": "rm -rf dist tsconfig.build.tsbuildinfo", + "build": "tsc --project ./tsconfig.build.json && npm run copy:dts", "copy:dts": "rsync -a --include='*/' --include='*.d.ts' --exclude='*' src/ dist/ || cpy 'src/**/*.d.ts' 'dist/' --parents", + "build:watch": "npx chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build' --initial", "typecheck": "tsc --noEmit && tsc -p example/convex", - "clean": "rm -rf dist tsconfig.build.tsbuildinfo", - "alpha": "npm run clean && npm run build && run-p test lint typecheck && npm version prerelease --preid alpha && npm publish --tag alpha && git push --tags", - "release": "npm run clean && npm run build && run-p test lint typecheck && npm version patch && npm publish && git push --tags && git push", + "lint": "eslint src && eslint example/convex", + "all": "run-p -r 'dev:backend' 'dev:frontend' 'build:watch' 'test:watch'", "test": "vitest run --typecheck", - "test:watch": "vitest --typecheck", + "test:watch": "vitest --typecheck --clearScreen false", "test:debug": "vitest --inspect-brk --no-file-parallelism", "test:coverage": "vitest run --coverage --coverage.reporter=text", - "lint": "eslint src && eslint example/convex", - "version": "pbcopy <<<$npm_package_version; vim CHANGELOG.md && git add CHANGELOG.md" + "attw": "attw $(npm pack -s) --exclude-entrypoints ./convex.config --profile esm-only", + "prepare": "npm run build", + "alpha": "npm run clean && npm ci && run-p test lint typecheck attw && npm version prerelease --preid alpha && npm publish --tag alpha && git push --tags", + "release": "npm run clean && npm ci && run-p test lint typecheck attw && npm version patch && npm publish && git push --tags", + "version": "pbcopy <<<$npm_package_version; vim CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md" }, "files": [ "dist", From 63b147f73ecd2b4e8612beb040058b3540aa4f5c Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 9 Oct 2025 20:05:51 -0700 Subject: [PATCH 17/52] use a pattern to break type dependency cycles --- example/convex/_generated/api.d.ts | 6 ++- example/convex/userConfirmation.ts | 50 --------------------- example/convex/userConfirmation/steps.ts | 31 +++++++++++++ example/convex/userConfirmation/workflow.ts | 30 +++++++++++++ 4 files changed, 65 insertions(+), 52 deletions(-) delete mode 100644 example/convex/userConfirmation.ts create mode 100644 example/convex/userConfirmation/steps.ts create mode 100644 example/convex/userConfirmation/workflow.ts diff --git a/example/convex/_generated/api.d.ts b/example/convex/_generated/api.d.ts index 45431cc..8e7a36a 100644 --- a/example/convex/_generated/api.d.ts +++ b/example/convex/_generated/api.d.ts @@ -11,7 +11,8 @@ import type * as admin from "../admin.js"; import type * as example from "../example.js"; import type * as transcription from "../transcription.js"; -import type * as userConfirmation from "../userConfirmation.js"; +import type * as userConfirmation_steps from "../userConfirmation/steps.js"; +import type * as userConfirmation_workflow from "../userConfirmation/workflow.js"; import type { ApiFromModules, @@ -31,7 +32,8 @@ declare const fullApi: ApiFromModules<{ admin: typeof admin; example: typeof example; transcription: typeof transcription; - userConfirmation: typeof userConfirmation; + "userConfirmation/steps": typeof userConfirmation_steps; + "userConfirmation/workflow": typeof userConfirmation_workflow; }>; declare const fullApiWithMounts: typeof fullApi; diff --git a/example/convex/userConfirmation.ts b/example/convex/userConfirmation.ts deleted file mode 100644 index e3c4c1f..0000000 --- a/example/convex/userConfirmation.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { defineEvent, vWorkflowId } from "@convex-dev/workflow"; -import { v } from "convex/values"; -import { internal } from "./_generated/api"; -import { internalAction, mutation } from "./_generated/server"; -import { workflow } from "./example"; - -const approvalEvent = defineEvent({ - name: "approval", - validator: v.union( - v.object({ approved: v.literal(true), choice: v.number() }), - v.object({ approved: v.literal(false), reason: v.string() }), - ), -}); - -export const confirmationWorkflow = workflow.define({ - args: { prompt: v.string() }, - returns: v.string(), - handler: async (step, args): Promise => { - const proposals = await step.runAction( - internal.userConfirmation.generateProposals, - { prompt: args.prompt }, - { retry: true }, - ); - const approval = await step.awaitEvent(approvalEvent); - if (!approval.approved) { - return "rejected: " + approval.reason; - } - const choice = proposals[approval.choice]; - return choice; - }, -}); - -export const generateProposals = internalAction({ - args: { prompt: v.string() }, - handler: async (_ctx, _args) => { - // imagine this is a call to an LLM - return ["proposal1", "proposal2", "proposal3"]; - }, -}); - -export const chooseProposal = mutation({ - args: { workflowId: vWorkflowId, choice: v.number() }, - handler: async (ctx, args) => { - await workflow.sendEvent(ctx, args.workflowId, approvalEvent, { - approved: true, - choice: args.choice, - }); - return true; - }, -}); diff --git a/example/convex/userConfirmation/steps.ts b/example/convex/userConfirmation/steps.ts new file mode 100644 index 0000000..8efe7d8 --- /dev/null +++ b/example/convex/userConfirmation/steps.ts @@ -0,0 +1,31 @@ +import { v } from "convex/values"; +import { internalAction, internalMutation } from "../_generated/server"; +import { workflow } from "../example"; +import { vWorkflowId, defineEvent } from "@convex-dev/workflow"; + +export const approvalEvent = defineEvent({ + name: "approval", + validator: v.union( + v.object({ approved: v.literal(true), choice: v.number() }), + v.object({ approved: v.literal(false), reason: v.string() }), + ), +}); + +export const generateProposals = internalAction({ + args: { prompt: v.string() }, + handler: async (_ctx, _args) => { + // imagine this is a call to an LLM + return ["proposal1", "proposal2", "proposal3"]; + }, +}); + +export const chooseProposal = internalMutation({ + args: { workflowId: vWorkflowId, choice: v.number() }, + handler: async (ctx, args) => { + await workflow.sendEvent(ctx, args.workflowId, approvalEvent, { + approved: true, + choice: args.choice, + }); + return true; + }, +}); diff --git a/example/convex/userConfirmation/workflow.ts b/example/convex/userConfirmation/workflow.ts new file mode 100644 index 0000000..d10daea --- /dev/null +++ b/example/convex/userConfirmation/workflow.ts @@ -0,0 +1,30 @@ +import { v } from "convex/values"; +import { anyApi, type ApiFromModules } from "convex/server"; +import { workflow } from "../example"; +import { approvalEvent } from "./steps"; + +// This allows us to scope an `internal` object to just the steps file, +// breaking circular dependency issues. +const steps = ( + anyApi as unknown as ApiFromModules<{ + "userConfirmation/steps": typeof import("./steps"); + }> +).userConfirmation.steps; + +export const confirmationWorkflow = workflow.define({ + args: { prompt: v.string() }, + returns: v.string(), + handler: async (ctx, args) => { + const proposals = await ctx.runAction( + steps.generateProposals, + { prompt: args.prompt }, + { retry: true }, + ); + const approval = await ctx.awaitEvent(approvalEvent); + if (!approval.approved) { + return "rejected: " + approval.reason; + } + const choice = proposals[approval.choice]; + return choice; + }, +}); From 524fdf955a7a457ad31746c18c538197ae02f478 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 05:50:56 +0000 Subject: [PATCH 18/52] Update dependency openai to v6.3.0 (#142) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e16db4..0ef8487 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "eslint": "9.37.0", "globals": "16.4.0", "npm-run-all2": "8.0.4", - "openai": "6.2.0", + "openai": "6.3.0", "pkg-pr-new": "0.0.60", "prettier": "3.6.2", "typescript": "5.9.3", @@ -3268,9 +3268,9 @@ } }, "node_modules/openai": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.2.0.tgz", - "integrity": "sha512-qqjzHls7F5xkXNGy9P1Ei1rorI5LWupUUFWP66zPU8FlZbiITX8SFcHMKNZg/NATJ0LpIZcMUFxSwQmdeQPwSw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.3.0.tgz", + "integrity": "sha512-E6vOGtZvdcb4yXQ5jXvDlUG599OhIkb/GjBLZXS+qk0HF+PJReIldEc9hM8Ft81vn+N6dRdFRb7BZNK8bbvXrw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 170508d..3044262 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "eslint": "9.37.0", "globals": "16.4.0", "npm-run-all2": "8.0.4", - "openai": "6.2.0", + "openai": "6.3.0", "pkg-pr-new": "0.0.60", "prettier": "3.6.2", "typescript": "5.9.3", From 7e4b0901f503048ed7b8e79f56dea8c504b1a962 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:50:34 +0000 Subject: [PATCH 19/52] Update dependency @convex-dev/workpool to v0.2.19-alpha.3 (#143) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ef8487..0927b39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "async-channel": "^0.2.0" }, "devDependencies": { - "@convex-dev/workpool": "0.2.19-alpha.2", + "@convex-dev/workpool": "0.2.19-alpha.3", "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", @@ -92,9 +92,9 @@ "license": "MIT" }, "node_modules/@convex-dev/workpool": { - "version": "0.2.19-alpha.2", - "resolved": "https://registry.npmjs.org/@convex-dev/workpool/-/workpool-0.2.19-alpha.2.tgz", - "integrity": "sha512-OrrU8x69SKTr3XbRRg4RUuOnRcD5Al3nii66OA48ZNYRJrswJ5SzZ9h9vKNYH7L66M3nqXCm9BEfiXbQMdvYUQ==", + "version": "0.2.19-alpha.3", + "resolved": "https://registry.npmjs.org/@convex-dev/workpool/-/workpool-0.2.19-alpha.3.tgz", + "integrity": "sha512-H3xNQI75huH5+2RNEEXUdlqQc8Xgjani2SUx5PG/I6B7ijc5XBFwjKa8afaXwFmYeg+IniRQ8vuTYFbX0pnxVw==", "dev": true, "license": "Apache-2.0", "peerDependencies": { diff --git a/package.json b/package.json index 3044262..06571fd 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "async-channel": "^0.2.0" }, "devDependencies": { - "@convex-dev/workpool": "0.2.19-alpha.2", + "@convex-dev/workpool": "0.2.19-alpha.3", "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", From 7750c90e791d6d823e9ae764398d4ca47bca9d02 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 20:45:41 +0000 Subject: [PATCH 20/52] Update dependency @types/node to v22.18.10 (#145) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0927b39..7ac61cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", - "@types/node": "22.18.9", + "@types/node": "22.18.10", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", @@ -1412,9 +1412,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.18.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.9.tgz", - "integrity": "sha512-5yBtK0k/q8PjkMXbTfeIEP/XVYnz1R9qZJ3yUicdEW7ppdDJfe+MqXEhpqDL3mtn4Wvs1u0KLEG0RXzCgNpsSg==", + "version": "22.18.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.10.tgz", + "integrity": "sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg==", "dev": true, "license": "MIT", "peer": true, diff --git a/package.json b/package.json index 06571fd..0ddea20 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", - "@types/node": "22.18.9", + "@types/node": "22.18.10", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", From 9780ec33833d65a009da70e9e7dceb54e041e952 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 04:48:16 +0000 Subject: [PATCH 21/52] Update dependency convex to v1.28.0 (#147) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ac61cc..69503cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", - "convex": "1.27.5", + "convex": "1.28.0", "convex-test": "0.0.38", "cpy-cli": "6.0.0", "eslint": "9.37.0", @@ -2136,9 +2136,9 @@ "license": "MIT" }, "node_modules/convex": { - "version": "1.27.5", - "resolved": "https://registry.npmjs.org/convex/-/convex-1.27.5.tgz", - "integrity": "sha512-6YU/AVPnoNdAaJABKBI9c5IqRSKsow/c4yo/ntaOWtd8Dff2P2zaImA/ougICfPgTuTvjKRbgkxk6lJhODzb4g==", + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.28.0.tgz", + "integrity": "sha512-40FgeJ/LxP9TxnkDDztU/A5gcGTdq1klcTT5mM0Ak+kSlQiDktMpjNX1TfkWLxXaE3lI4qvawKH95v2RiYgFxA==", "license": "Apache-2.0", "peer": true, "dependencies": { diff --git a/package.json b/package.json index 0ddea20..be86336 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", - "convex": "1.27.5", + "convex": "1.28.0", "convex-test": "0.0.38", "cpy-cli": "6.0.0", "eslint": "9.37.0", From a4b293d4c98ad1974c68f0d2732bfece6106d085 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Wed, 15 Oct 2025 23:19:37 -0700 Subject: [PATCH 22/52] improve test entrypoint --- src/test.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/test.ts b/src/test.ts index fef4c82..0a3ceb6 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,3 +1,17 @@ +import type { TestConvex } from "convex-test"; +import type { GenericSchema, SchemaDefinition } from "convex/server"; import schema from "./component/schema.js"; -const modules = import.meta.glob("./**/*.ts"); -export default { schema, modules }; +const modules = import.meta.glob("./component/**/*.ts"); + +/** + * Register the component with the test convex instance. + * @param t - The test convex instance, e.g. from calling `convexTest`. + * @param name - The name of the component, as registered in convex.config.ts. + */ +function register( + t: TestConvex>, + name: string, +) { + t.registerComponent(name, schema, modules); +} +export default { register, schema, modules }; From a64fa268f5380a66f62ca00c49dfdd5f9887a4f9 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Wed, 15 Oct 2025 23:19:48 -0700 Subject: [PATCH 23/52] improve opaqueids --- src/client/types.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/types.ts b/src/client/types.ts index e8ade97..1f3fe1a 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -132,8 +132,10 @@ export type UseApi = Expand<{ export type OpaqueIds = T extends GenericId ? string - : T extends WorkId | WorkflowId | EventId - ? string + : T extends string + ? `${T}` extends T + ? T + : string : T extends (infer U)[] ? OpaqueIds[] : T extends object From f015e2aba599c54f51c3a4f6f4234c9d520ffac5 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Wed, 15 Oct 2025 23:20:36 -0700 Subject: [PATCH 24/52] drop attw --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 4551865..96ab86f 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,9 @@ "test:watch": "vitest --typecheck --clearScreen false", "test:debug": "vitest --inspect-brk --no-file-parallelism", "test:coverage": "vitest run --coverage --coverage.reporter=text", - "attw": "attw $(npm pack -s) --exclude-entrypoints ./convex.config --profile esm-only", "prepare": "npm run build", - "alpha": "npm run clean && npm ci && run-p test lint typecheck attw && npm version prerelease --preid alpha && npm publish --tag alpha && git push --tags", - "release": "npm run clean && npm ci && run-p test lint typecheck attw && npm version patch && npm publish && git push --tags", + "alpha": "npm run clean && npm ci && run-p test lint typecheck && npm version prerelease --preid alpha && npm publish --tag alpha && git push --tags", + "release": "npm run clean && npm ci && run-p test lint typecheck && npm version patch && npm publish && git push --tags", "version": "pbcopy <<<$npm_package_version; vim CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md" }, "files": [ From 3d7b10712711856d7b2c2fed03e1f5b030becec0 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Wed, 15 Oct 2025 23:32:47 -0700 Subject: [PATCH 25/52] 0.2.8-alpha.4 --- CHANGELOG.md | 1 + package-lock.json | 14 +++++++------- package.json | 5 ++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c52d77..536e01f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Reduces read bandwidth when reading the journal after running many steps in parallel. - Simplifies the onComplete type requirement so you can accept a workflowId as a string. This helps when you have statically generated types which can't do branded strings. +- Adds a /test entrypoint to make testing easier ## 0.2.7 diff --git a/package-lock.json b/package-lock.json index 8ba2538..9adc3d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.3", + "version": "0.2.8-alpha.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.3", + "version": "0.2.8-alpha.4", "license": "Apache-2.0", "dependencies": { "async-channel": "^0.2.0" }, "devDependencies": { - "@convex-dev/workpool": "^0.2.19-alpha.2", + "@convex-dev/workpool": "^0.2.19", "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", @@ -34,7 +34,7 @@ "vitest": "3.2.4" }, "peerDependencies": { - "@convex-dev/workpool": "^0.2.18", + "@convex-dev/workpool": "^0.2.19", "convex": ">=1.25.0 <1.35.0", "convex-helpers": "^0.1.99" } @@ -92,9 +92,9 @@ "license": "MIT" }, "node_modules/@convex-dev/workpool": { - "version": "0.2.19-alpha.2", - "resolved": "https://registry.npmjs.org/@convex-dev/workpool/-/workpool-0.2.19-alpha.2.tgz", - "integrity": "sha512-OrrU8x69SKTr3XbRRg4RUuOnRcD5Al3nii66OA48ZNYRJrswJ5SzZ9h9vKNYH7L66M3nqXCm9BEfiXbQMdvYUQ==", + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@convex-dev/workpool/-/workpool-0.2.19.tgz", + "integrity": "sha512-U2KwYnsKILyxW1baWEhDv+ZtnL5FZbYFxBT5owQ0Lw/kseiudMZraA4clH+/6gowHSahWpkq4wndhcOfpfhuOA==", "dev": true, "license": "Apache-2.0", "peerDependencies": { diff --git a/package.json b/package.json index 96ab86f..508f559 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.3", + "version": "0.2.8-alpha.4", "description": "Convex component for durably executing workflows.", "keywords": [ "convex", @@ -52,7 +52,7 @@ } }, "peerDependencies": { - "@convex-dev/workpool": "^0.2.18", + "@convex-dev/workpool": "^0.2.19", "convex": ">=1.25.0 <1.35.0", "convex-helpers": "^0.1.99" }, @@ -60,7 +60,6 @@ "async-channel": "^0.2.0" }, "devDependencies": { - "@convex-dev/workpool": "^0.2.19-alpha.2", "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", From ff5a4490cd3c6855a724157709936554a5b1894e Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Wed, 15 Oct 2025 23:33:31 -0700 Subject: [PATCH 26/52] default test register --- src/test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test.ts b/src/test.ts index 0a3ceb6..83225c1 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,5 +1,6 @@ import type { TestConvex } from "convex-test"; import type { GenericSchema, SchemaDefinition } from "convex/server"; +import workpool from "@convex-dev/workpool/test"; import schema from "./component/schema.js"; const modules = import.meta.glob("./component/**/*.ts"); @@ -10,8 +11,9 @@ const modules = import.meta.glob("./component/**/*.ts"); */ function register( t: TestConvex>, - name: string, + name: string = "workflow", ) { t.registerComponent(name, schema, modules); + workpool.register(t, "workpool"); } export default { register, schema, modules }; From 678467d17f3b5c7b859ef8b018ef86cdea9466af Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Wed, 15 Oct 2025 23:34:07 -0700 Subject: [PATCH 27/52] improve opaqueIds --- src/client/types.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/types.ts b/src/client/types.ts index 1f3fe1a..92e119b 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -138,6 +138,8 @@ export type OpaqueIds = : string : T extends (infer U)[] ? OpaqueIds[] - : T extends object - ? { [K in keyof T]: OpaqueIds } - : T; + : T extends ArrayBuffer + ? ArrayBuffer + : T extends object + ? { [K in keyof T]: OpaqueIds } + : T; From 6c063a5a6cb23b7a51935c437550abb8e30571bd Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Wed, 15 Oct 2025 23:34:25 -0700 Subject: [PATCH 28/52] 0.2.8-alpha.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9adc3d0..003046a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.4", + "version": "0.2.8-alpha.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.4", + "version": "0.2.8-alpha.5", "license": "Apache-2.0", "dependencies": { "async-channel": "^0.2.0" diff --git a/package.json b/package.json index 508f559..7651d6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.4", + "version": "0.2.8-alpha.5", "description": "Convex component for durably executing workflows.", "keywords": [ "convex", From 90c9fea62203b352fb10139b4b4f37955e05c02a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 06:34:58 +0000 Subject: [PATCH 29/52] Update dependency @convex-dev/workpool to v0.2.19 (#148) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 69503cd..ffa4daf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "async-channel": "^0.2.0" }, "devDependencies": { - "@convex-dev/workpool": "0.2.19-alpha.3", + "@convex-dev/workpool": "0.2.19", "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", @@ -92,9 +92,9 @@ "license": "MIT" }, "node_modules/@convex-dev/workpool": { - "version": "0.2.19-alpha.3", - "resolved": "https://registry.npmjs.org/@convex-dev/workpool/-/workpool-0.2.19-alpha.3.tgz", - "integrity": "sha512-H3xNQI75huH5+2RNEEXUdlqQc8Xgjani2SUx5PG/I6B7ijc5XBFwjKa8afaXwFmYeg+IniRQ8vuTYFbX0pnxVw==", + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@convex-dev/workpool/-/workpool-0.2.19.tgz", + "integrity": "sha512-U2KwYnsKILyxW1baWEhDv+ZtnL5FZbYFxBT5owQ0Lw/kseiudMZraA4clH+/6gowHSahWpkq4wndhcOfpfhuOA==", "dev": true, "license": "Apache-2.0", "peerDependencies": { diff --git a/package.json b/package.json index be86336..9556b4d 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "async-channel": "^0.2.0" }, "devDependencies": { - "@convex-dev/workpool": "0.2.19-alpha.3", + "@convex-dev/workpool": "0.2.19", "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", From 82d984fd1b005fde487173cd5c48e8c8a2e1b5bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 00:30:27 +0000 Subject: [PATCH 30/52] Update dependency openai to v6.4.0 (#149) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index ffa4daf..66579a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "eslint": "9.37.0", "globals": "16.4.0", "npm-run-all2": "8.0.4", - "openai": "6.3.0", + "openai": "6.4.0", "pkg-pr-new": "0.0.60", "prettier": "3.6.2", "typescript": "5.9.3", @@ -3268,9 +3268,9 @@ } }, "node_modules/openai": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.3.0.tgz", - "integrity": "sha512-E6vOGtZvdcb4yXQ5jXvDlUG599OhIkb/GjBLZXS+qk0HF+PJReIldEc9hM8Ft81vn+N6dRdFRb7BZNK8bbvXrw==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.4.0.tgz", + "integrity": "sha512-vSoVBRTPMgg3oSaoHIGfbYM2zwGN0D4F2aiNHMeu1lZHFOwfJMAF0X110HDdedYvcsIo578ujQ11WL5kP687Cw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 9556b4d..0a04719 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "eslint": "9.37.0", "globals": "16.4.0", "npm-run-all2": "8.0.4", - "openai": "6.3.0", + "openai": "6.4.0", "pkg-pr-new": "0.0.60", "prettier": "3.6.2", "typescript": "5.9.3", From 5077927c53e254dca97af6a0afaf92f5fd8164dc Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 16 Oct 2025 23:59:03 -0700 Subject: [PATCH 31/52] export WorkflowCtx --- src/client/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/index.ts b/src/client/index.ts index f41e4c4..bec80df 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -35,7 +35,7 @@ export { vWorkflowStep, type WorkflowStep, } from "../types.js"; -export type { RunOptions } from "./types.js"; +export type { RunOptions, WorkflowCtx } from "./types.js"; export { defineEvent } from "./events.js"; export type CallbackOptions = { From 60936f9af9044e10aaf3b4106c071b1f19c954a4 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Thu, 16 Oct 2025 23:59:39 -0700 Subject: [PATCH 32/52] 0.2.8-alpha.6 --- CHANGELOG.md | 1 + package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 536e01f..efbcba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Simplifies the onComplete type requirement so you can accept a workflowId as a string. This helps when you have statically generated types which can't do branded strings. - Adds a /test entrypoint to make testing easier +- Exports the `WorkflowCtx` and `WorkflowStep` types ## 0.2.7 diff --git a/package-lock.json b/package-lock.json index 003046a..12ea64c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.5", + "version": "0.2.8-alpha.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.5", + "version": "0.2.8-alpha.6", "license": "Apache-2.0", "dependencies": { "async-channel": "^0.2.0" diff --git a/package.json b/package.json index 7651d6e..7c71a38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.5", + "version": "0.2.8-alpha.6", "description": "Convex component for durably executing workflows.", "keywords": [ "convex", From 2f73c703c79e715d91f3db6a6330b9d51f666fb8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 07:00:13 +0000 Subject: [PATCH 33/52] Update dependency @types/node to v22.18.11 (#150) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 66579a1..71b36ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", - "@types/node": "22.18.10", + "@types/node": "22.18.11", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", @@ -1412,9 +1412,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.18.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.10.tgz", - "integrity": "sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg==", + "version": "22.18.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.11.tgz", + "integrity": "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ==", "dev": true, "license": "MIT", "peer": true, diff --git a/package.json b/package.json index 0a04719..65b3a34 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", - "@types/node": "22.18.10", + "@types/node": "22.18.11", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", From bdf2e71c2a6a5b1166df7c359812c35beaf24904 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Fri, 17 Oct 2025 11:23:10 -0700 Subject: [PATCH 34/52] fix nested workflows --- example/convex/_generated/api.d.ts | 2 ++ example/convex/nestedWorkflow.ts | 25 +++++++++++++++++++++++++ src/client/index.ts | 11 +++++++++-- src/client/types.ts | 2 +- 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 example/convex/nestedWorkflow.ts diff --git a/example/convex/_generated/api.d.ts b/example/convex/_generated/api.d.ts index 8e7a36a..7553f9b 100644 --- a/example/convex/_generated/api.d.ts +++ b/example/convex/_generated/api.d.ts @@ -10,6 +10,7 @@ import type * as admin from "../admin.js"; import type * as example from "../example.js"; +import type * as nestedWorkflow from "../nestedWorkflow.js"; import type * as transcription from "../transcription.js"; import type * as userConfirmation_steps from "../userConfirmation/steps.js"; import type * as userConfirmation_workflow from "../userConfirmation/workflow.js"; @@ -31,6 +32,7 @@ import type { declare const fullApi: ApiFromModules<{ admin: typeof admin; example: typeof example; + nestedWorkflow: typeof nestedWorkflow; transcription: typeof transcription; "userConfirmation/steps": typeof userConfirmation_steps; "userConfirmation/workflow": typeof userConfirmation_workflow; diff --git a/example/convex/nestedWorkflow.ts b/example/convex/nestedWorkflow.ts new file mode 100644 index 0000000..952828a --- /dev/null +++ b/example/convex/nestedWorkflow.ts @@ -0,0 +1,25 @@ +import { v } from "convex/values"; +import { workflow } from "./example"; +import { internal } from "./_generated/api"; + +export const parentWorkflow = workflow.define({ + args: { prompt: v.string() }, + returns: v.null(), + handler: async (ctx, args) => { + console.log("Starting confirmation workflow"); + const length = await ctx.runWorkflow( + internal.nestedWorkflow.nestedWorkflow, + { foo: args.prompt }, + ); + console.log("Length:", length); + }, +}); + +export const nestedWorkflow = workflow.define({ + args: { foo: v.string() }, + returns: v.number(), + handler: async (_, args) => { + console.log("Starting nested workflow"); + return args.foo.length; + }, +}); diff --git a/src/client/index.ts b/src/client/index.ts index bec80df..a64c685 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -15,7 +15,12 @@ import { type RegisteredMutation, type ReturnValueForOptionalValidator, } from "convex/server"; -import type { ObjectType, PropertyValidators, Validator } from "convex/values"; +import type { + Infer, + ObjectType, + PropertyValidators, + Validator, +} from "convex/values"; import type { Step } from "../component/schema.js"; import type { EventId, @@ -113,7 +118,9 @@ export class WorkflowManager { fn: "You should not call this directly, call workflow.start instead"; args: ObjectType; }, - void + ReturnsValidator extends Validator + ? Infer + : void > { return workflowMutation( this.component, diff --git a/src/client/types.ts b/src/client/types.ts index 92e119b..1d96442 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -91,7 +91,7 @@ export type WorkflowCtx = { */ runWorkflow>( workflow: Workflow, - args: FunctionArgs, + args: FunctionArgs["args"], opts?: RunOptions, ): Promise>; From 4a34800404ba0cfe5ae9be7ae9a92df18e237c4e Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Fri, 17 Oct 2025 14:07:13 -0700 Subject: [PATCH 35/52] add step to example --- example/convex/nestedWorkflow.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/example/convex/nestedWorkflow.ts b/example/convex/nestedWorkflow.ts index 952828a..ae4590f 100644 --- a/example/convex/nestedWorkflow.ts +++ b/example/convex/nestedWorkflow.ts @@ -1,25 +1,37 @@ import { v } from "convex/values"; import { workflow } from "./example"; import { internal } from "./_generated/api"; +import { internalMutation } from "./_generated/server"; export const parentWorkflow = workflow.define({ args: { prompt: v.string() }, - returns: v.null(), handler: async (ctx, args) => { console.log("Starting confirmation workflow"); const length = await ctx.runWorkflow( - internal.nestedWorkflow.nestedWorkflow, + internal.nestedWorkflow.childWorkflow, { foo: args.prompt }, ); console.log("Length:", length); + const stepResult = await ctx.runMutation(internal.nestedWorkflow.step, { + foo: args.prompt, + }); + console.log("Step result:", stepResult); }, }); -export const nestedWorkflow = workflow.define({ +export const childWorkflow = workflow.define({ args: { foo: v.string() }, returns: v.number(), - handler: async (_, args) => { + handler: async (_ctx, args) => { console.log("Starting nested workflow"); return args.foo.length; }, }); + +export const step = internalMutation({ + args: { foo: v.string() }, + handler: async (_ctx, args) => { + console.log("Starting step"); + return args.foo.length; + }, +}); From 72cb23cfa1b3374c582da330e5ea6e9e2fefe2d2 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Fri, 17 Oct 2025 14:07:49 -0700 Subject: [PATCH 36/52] 0.2.8-alpha.7 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 12ea64c..20a3850 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.6", + "version": "0.2.8-alpha.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.6", + "version": "0.2.8-alpha.7", "license": "Apache-2.0", "dependencies": { "async-channel": "^0.2.0" diff --git a/package.json b/package.json index 7c71a38..11b3a65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.6", + "version": "0.2.8-alpha.7", "description": "Convex component for durably executing workflows.", "keywords": [ "convex", From aacf9a27f9903ff3e8a8ab6f1aec40f388de8244 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 21:11:09 +0000 Subject: [PATCH 37/52] Update dependency openai to v6.5.0 (#151) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71b36ed..63ad9e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "eslint": "9.37.0", "globals": "16.4.0", "npm-run-all2": "8.0.4", - "openai": "6.4.0", + "openai": "6.5.0", "pkg-pr-new": "0.0.60", "prettier": "3.6.2", "typescript": "5.9.3", @@ -3268,9 +3268,9 @@ } }, "node_modules/openai": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.4.0.tgz", - "integrity": "sha512-vSoVBRTPMgg3oSaoHIGfbYM2zwGN0D4F2aiNHMeu1lZHFOwfJMAF0X110HDdedYvcsIo578ujQ11WL5kP687Cw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.5.0.tgz", + "integrity": "sha512-bNqJ15Ijbs41KuJ2iYz/mGAruFHzQQt7zXo4EvjNLoB64aJdgn1jlMeDTsXjEg+idVYafg57QB/5Rd16oqvZ6A==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 65b3a34..a278648 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "eslint": "9.37.0", "globals": "16.4.0", "npm-run-all2": "8.0.4", - "openai": "6.4.0", + "openai": "6.5.0", "pkg-pr-new": "0.0.60", "prettier": "3.6.2", "typescript": "5.9.3", From 5aa66cfab316373c3b5483e2993f338482c42436 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Fri, 17 Oct 2025 14:16:50 -0700 Subject: [PATCH 38/52] fix test registration of child workpool --- src/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test.ts b/src/test.ts index 83225c1..7a340a7 100644 --- a/src/test.ts +++ b/src/test.ts @@ -14,6 +14,6 @@ function register( name: string = "workflow", ) { t.registerComponent(name, schema, modules); - workpool.register(t, "workpool"); + workpool.register(t, `${name}/workpool`); } export default { register, schema, modules }; From 9e57c2755a471d59893c09b7a5395c690d3e9e3d Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Fri, 17 Oct 2025 14:20:35 -0700 Subject: [PATCH 39/52] unused types --- src/client/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/types.ts b/src/client/types.ts index 1d96442..2427db1 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -1,4 +1,4 @@ -import type { RetryOption, WorkId } from "@convex-dev/workpool"; +import type { RetryOption } from "@convex-dev/workpool"; import type { Expand, FunctionArgs, @@ -7,7 +7,7 @@ import type { } from "convex/server"; import type { api } from "../component/_generated/api.js"; import type { GenericId } from "convex/values"; -import type { EventId, EventSpec, WorkflowId } from "../types.js"; +import type { EventSpec, WorkflowId } from "../types.js"; export type WorkflowComponent = UseApi; From 3e7a7be4004b399f1cd556ed2762be82a1d109ae Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Fri, 17 Oct 2025 14:20:39 -0700 Subject: [PATCH 40/52] 0.2.8-alpha.8 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 20a3850..14635c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.7", + "version": "0.2.8-alpha.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.7", + "version": "0.2.8-alpha.8", "license": "Apache-2.0", "dependencies": { "async-channel": "^0.2.0" diff --git a/package.json b/package.json index 11b3a65..0b08e11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.7", + "version": "0.2.8-alpha.8", "description": "Convex component for durably executing workflows.", "keywords": [ "convex", From 1b2ffccfe0c28c300f63cdbd2185cd76656791dd Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Fri, 17 Oct 2025 18:26:34 -0700 Subject: [PATCH 41/52] allow passing just eventId on send --- example/convex/_generated/api.d.ts | 2 +- src/component/_generated/api.d.ts | 2 +- src/component/event.ts | 18 ++++++++++-------- src/types.ts | 22 +++++++--------------- 4 files changed, 19 insertions(+), 25 deletions(-) diff --git a/example/convex/_generated/api.d.ts b/example/convex/_generated/api.d.ts index 7553f9b..322a24a 100644 --- a/example/convex/_generated/api.d.ts +++ b/example/convex/_generated/api.d.ts @@ -67,7 +67,7 @@ export declare const components: { | { kind: "success"; returnValue: any } | { error: string; kind: "failed" } | { kind: "canceled" }; - workflowId: string; + workflowId?: string; workpoolOptions?: { defaultRetryBehavior?: { base: number; diff --git a/src/component/_generated/api.d.ts b/src/component/_generated/api.d.ts index af0b54d..4a58fb0 100644 --- a/src/component/_generated/api.d.ts +++ b/src/component/_generated/api.d.ts @@ -57,7 +57,7 @@ export type Mounts = { | { kind: "success"; returnValue: any } | { error: string; kind: "failed" } | { kind: "canceled" }; - workflowId: string; + workflowId?: string; workpoolOptions?: { defaultRetryBehavior?: { base: number; diff --git a/src/component/event.ts b/src/component/event.ts index 6f608e8..a002316 100644 --- a/src/component/event.ts +++ b/src/component/event.ts @@ -65,7 +65,7 @@ export async function awaitEvent( async function getOrCreateEvent( ctx: MutationCtx, - workflowId: Id<"workflows">, + workflowId: Id<"workflows"> | undefined, args: { eventId?: Id<"events">; name?: string }, statuses: Doc<"events">["state"]["kind"][], ): Promise> { @@ -79,6 +79,7 @@ async function getOrCreateEvent( return event; } assert(args.name, "Name is required if eventId is not specified"); + assert(workflowId, "workflowId is required if eventId is not specified"); for (const status of statuses) { const event = await ctx.db .query("events") @@ -101,7 +102,7 @@ async function getOrCreateEvent( export const send = mutation({ args: { - workflowId: v.id("workflows"), + workflowId: v.optional(v.id("workflows")), eventId: v.optional(v.id("events")), name: v.optional(v.string()), result: vResultValidator, @@ -118,16 +119,17 @@ export const send = mutation({ }, ["waiting", "created"], ); + const { workflowId } = event; const name = args.name ?? event.name; switch (event.state.kind) { case "sent": { throw new Error( - `Event already sent: ${event._id} (${name}) in workflow ${args.workflowId}`, + `Event already sent: ${event._id} (${name}) in workflow ${workflowId}`, ); } case "consumed": { throw new Error( - `Event already consumed: ${event._id} (${name}) in workflow ${args.workflowId}`, + `Event already consumed: ${event._id} (${name}) in workflow ${workflowId}`, ); } case "created": { @@ -140,7 +142,7 @@ export const send = mutation({ const step = await ctx.db.get(event.state.stepId); assert( step, - `Entry ${event.state.stepId} not found when sending event ${event._id} (${name}) in workflow ${args.workflowId}`, + `Entry ${event.state.stepId} not found when sending event ${event._id} (${name}) in workflow ${workflowId}`, ); assert(step.step.kind === "event", "Step is not an event"); step.step.eventId = event._id; @@ -160,13 +162,13 @@ export const send = mutation({ const anyMoreEvents = await ctx.db .query("events") .withIndex("workflowId_state", (q) => - q.eq("workflowId", args.workflowId).eq("state.kind", "waiting"), + q.eq("workflowId", workflowId).eq("state.kind", "waiting"), ) .order("desc") .first(); if (!anyMoreEvents) { - const workflow = await ctx.db.get(args.workflowId); - assert(workflow, `Workflow ${args.workflowId} not found`); + const workflow = await ctx.db.get(workflowId); + assert(workflow, `Workflow ${workflowId} not found`); const workpool = await getWorkpool(ctx, args.workpoolOptions); await enqueueWorkflow(ctx, workflow, workpool); } diff --git a/src/types.ts b/src/types.ts index b1614e2..0520345 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,10 +22,11 @@ export type EventId = string & { export type VEventId = VString>; export const vEventId = v.string() as VString>; -export type EventSpec = { - name: Name; +export type EventSpec = ( + | { name: Name; id?: EventId } + | { name?: Name; id: EventId } +) & { validator?: Validator; - id?: EventId; }; export type WorkflowStep = { @@ -40,18 +41,9 @@ export type WorkflowStep = { startedAt: number; completedAt?: number; } & ( - | { - kind: "function"; - workId: WorkId; - } - | { - kind: "workflow"; - nestedWorkflowId: WorkflowId; - } - | { - kind: "event"; - eventId: EventId; - } + | { kind: "function"; workId: WorkId } + | { kind: "workflow"; nestedWorkflowId: WorkflowId } + | { kind: "event"; eventId: EventId } ); export const vWorkflowStep = v.object({ From b89d442b798f8451ba3d944def0369e6358c8775 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Fri, 17 Oct 2025 18:27:26 -0700 Subject: [PATCH 42/52] pass object args to sendEvent --- src/client/index.ts | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/client/index.ts b/src/client/index.ts index a64c685..8fc00ec 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,4 +1,5 @@ import type { + RunResult, WorkpoolOptions, WorkpoolRetryOptions, } from "@convex-dev/workpool"; @@ -267,24 +268,29 @@ export class WorkflowManager { */ async sendEvent( ctx: RunMutationCtx, - workflowId: WorkflowId, - args: EventSpec, - ...runResult: T extends null ? [] : [T] + { + workflowId, + event, + value, + }: ( + | { workflowId: WorkflowId; event: EventSpec } + | { workflowId?: undefined; event: EventSpec & { id: string } } + ) & + ( + | (T extends null + ? { value?: null; error?: undefined } + : { value: T; error?: undefined }) + | { error: string; value?: undefined } + ), ): Promise> { let result = { kind: "success" as const, - returnValue: runResult[0] ?? (null as T), - }; - if (args.validator && result.kind === "success") { - result = { - ...result, - returnValue: parse(args.validator, result.returnValue), - }; - } + returnValue: event.validator ? parse(event.validator, value) : value, + } satisfies RunResult; return (await ctx.runMutation(this.component.event.send, { - eventId: args.id, + eventId: event.id, result, - name: args.name, + name: event.name, workflowId: workflowId, workpoolOptions: this.options?.workpoolOptions, })) as EventId; From 53ffbd93e36ad55842a2b5e4eeb94f21fda2469b Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Sat, 18 Oct 2025 16:56:06 -0700 Subject: [PATCH 43/52] vEventId is a function --- src/types.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/types.ts b/src/types.ts index 0520345..f8157e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,14 +20,8 @@ export type EventId = string & { __name: Name; }; export type VEventId = VString>; -export const vEventId = v.string() as VString>; - -export type EventSpec = ( - | { name: Name; id?: EventId } - | { name?: Name; id: EventId } -) & { - validator?: Validator; -}; +export const vEventId = (_name?: Name) => + v.string() as VString>; export type WorkflowStep = { workflowId: WorkflowId; @@ -65,7 +59,7 @@ export const vWorkflowStep = v.object({ ), workId: v.optional(vWorkIdValidator), nestedWorkflowId: v.optional(vWorkflowId), - eventId: v.optional(vEventId), + eventId: v.optional(vEventId()), }); // type assertion to keep us in check const _: Infer = {} as WorkflowStep; From 17b83fa576f59e71a081a41e754ddfa6714ea843 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Sat, 18 Oct 2025 16:55:52 -0700 Subject: [PATCH 44/52] pass in spread object and reorganize workflowContext --- example/convex/_generated/api.d.ts | 8 +- example/convex/passingSignals.ts | 45 +++++ example/convex/userConfirmation.ts | 59 ++++++ example/convex/userConfirmation/steps.ts | 31 ---- example/convex/userConfirmation/workflow.ts | 30 ---- src/client/events.ts | 35 ---- src/client/index.ts | 95 +++++++--- src/client/step.ts | 4 +- src/client/stepContext.ts | 117 ------------ src/client/types.ts | 109 +---------- src/client/workflowContext.ts | 189 ++++++++++++++++++++ src/client/workflowMutation.ts | 4 +- src/component/workflow.ts | 2 +- src/types.ts | 18 ++ 14 files changed, 389 insertions(+), 357 deletions(-) create mode 100644 example/convex/passingSignals.ts create mode 100644 example/convex/userConfirmation.ts delete mode 100644 example/convex/userConfirmation/steps.ts delete mode 100644 example/convex/userConfirmation/workflow.ts delete mode 100644 src/client/events.ts delete mode 100644 src/client/stepContext.ts create mode 100644 src/client/workflowContext.ts diff --git a/example/convex/_generated/api.d.ts b/example/convex/_generated/api.d.ts index 322a24a..02f0590 100644 --- a/example/convex/_generated/api.d.ts +++ b/example/convex/_generated/api.d.ts @@ -11,9 +11,9 @@ import type * as admin from "../admin.js"; import type * as example from "../example.js"; import type * as nestedWorkflow from "../nestedWorkflow.js"; +import type * as passingSignals from "../passingSignals.js"; import type * as transcription from "../transcription.js"; -import type * as userConfirmation_steps from "../userConfirmation/steps.js"; -import type * as userConfirmation_workflow from "../userConfirmation/workflow.js"; +import type * as userConfirmation from "../userConfirmation.js"; import type { ApiFromModules, @@ -33,9 +33,9 @@ declare const fullApi: ApiFromModules<{ admin: typeof admin; example: typeof example; nestedWorkflow: typeof nestedWorkflow; + passingSignals: typeof passingSignals; transcription: typeof transcription; - "userConfirmation/steps": typeof userConfirmation_steps; - "userConfirmation/workflow": typeof userConfirmation_workflow; + userConfirmation: typeof userConfirmation; }>; declare const fullApiWithMounts: typeof fullApi; diff --git a/example/convex/passingSignals.ts b/example/convex/passingSignals.ts new file mode 100644 index 0000000..c6b947b --- /dev/null +++ b/example/convex/passingSignals.ts @@ -0,0 +1,45 @@ +import { vWorkflowId, WorkflowManager } from "@convex-dev/workflow"; +import { components, internal } from "./_generated/api"; +import { internalMutation } from "./_generated/server"; +import { vEventId } from "../../src/types"; + +const workflow = new WorkflowManager(components.workflow); + +export const signalBasedWorkflow = workflow.define({ + args: {}, + handler: async (ctx) => { + console.log("Starting signal based workflow"); + for (let i = 0; i < 3; i++) { + const signalId = await ctx.runMutation( + internal.passingSignals.createSignal, + { workflowId: ctx.workflowId }, + ); + await ctx.awaitEvent({ id: signalId }); + console.log("Signal received", signalId); + } + console.log("All signals received"); + }, +}); + +export const createSignal = internalMutation({ + args: { workflowId: vWorkflowId }, + handler: async (ctx, args) => { + const eventId = await workflow.createEvent(ctx, { + name: "signal", + workflowId: args.workflowId, + }); + // You would normally store this eventId somewhere to be able to send the + // signal later. + await ctx.scheduler.runAfter(1000, internal.passingSignals.sendSignal, { + eventId, + }); + return eventId; + }, +}); + +export const sendSignal = internalMutation({ + args: { eventId: vEventId("signal") }, + handler: async (ctx, args) => { + await workflow.sendEvent(ctx, { id: args.eventId }); + }, +}); diff --git a/example/convex/userConfirmation.ts b/example/convex/userConfirmation.ts new file mode 100644 index 0000000..fd8892b --- /dev/null +++ b/example/convex/userConfirmation.ts @@ -0,0 +1,59 @@ +import { + defineEvent, + vWorkflowId, + WorkflowManager, +} from "@convex-dev/workflow"; +import { v } from "convex/values"; +import { components, internal } from "./_generated/api"; +import { internalAction, internalMutation } from "./_generated/server"; + +export const approvalEvent = defineEvent({ + name: "approval" as const, + validator: v.union( + v.object({ approved: v.literal(true), choice: v.number() }), + v.object({ approved: v.literal(false), reason: v.string() }), + ), +}); + +const workflow = new WorkflowManager(components.workflow); + +export const confirmationWorkflow = workflow.define({ + args: { prompt: v.string() }, + returns: v.string(), + handler: async (ctx, args): Promise => { + console.log("Starting confirmation workflow"); + const proposals = await ctx.runAction( + internal.userConfirmation.generateProposals, + { prompt: args.prompt }, + { retry: true }, + ); + console.log("Proposals generated", proposals); + const approval = await ctx.awaitEvent(approvalEvent); + if (!approval.approved) { + return "rejected: " + approval.reason; + } + const choice = proposals[approval.choice]; + console.log("Choice selected", choice); + return choice; + }, +}); + +export const generateProposals = internalAction({ + args: { prompt: v.string() }, + handler: async (_ctx, _args) => { + // imagine this is a call to an LLM + return ["proposal1", "proposal2", "proposal3"]; + }, +}); + +export const chooseProposal = internalMutation({ + args: { workflowId: vWorkflowId, choice: v.number() }, + handler: async (ctx, args) => { + await workflow.sendEvent(ctx, { + ...approvalEvent, + workflowId: args.workflowId, + value: { approved: true, choice: args.choice }, + }); + return true; + }, +}); diff --git a/example/convex/userConfirmation/steps.ts b/example/convex/userConfirmation/steps.ts deleted file mode 100644 index 8efe7d8..0000000 --- a/example/convex/userConfirmation/steps.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { v } from "convex/values"; -import { internalAction, internalMutation } from "../_generated/server"; -import { workflow } from "../example"; -import { vWorkflowId, defineEvent } from "@convex-dev/workflow"; - -export const approvalEvent = defineEvent({ - name: "approval", - validator: v.union( - v.object({ approved: v.literal(true), choice: v.number() }), - v.object({ approved: v.literal(false), reason: v.string() }), - ), -}); - -export const generateProposals = internalAction({ - args: { prompt: v.string() }, - handler: async (_ctx, _args) => { - // imagine this is a call to an LLM - return ["proposal1", "proposal2", "proposal3"]; - }, -}); - -export const chooseProposal = internalMutation({ - args: { workflowId: vWorkflowId, choice: v.number() }, - handler: async (ctx, args) => { - await workflow.sendEvent(ctx, args.workflowId, approvalEvent, { - approved: true, - choice: args.choice, - }); - return true; - }, -}); diff --git a/example/convex/userConfirmation/workflow.ts b/example/convex/userConfirmation/workflow.ts deleted file mode 100644 index d10daea..0000000 --- a/example/convex/userConfirmation/workflow.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { v } from "convex/values"; -import { anyApi, type ApiFromModules } from "convex/server"; -import { workflow } from "../example"; -import { approvalEvent } from "./steps"; - -// This allows us to scope an `internal` object to just the steps file, -// breaking circular dependency issues. -const steps = ( - anyApi as unknown as ApiFromModules<{ - "userConfirmation/steps": typeof import("./steps"); - }> -).userConfirmation.steps; - -export const confirmationWorkflow = workflow.define({ - args: { prompt: v.string() }, - returns: v.string(), - handler: async (ctx, args) => { - const proposals = await ctx.runAction( - steps.generateProposals, - { prompt: args.prompt }, - { retry: true }, - ); - const approval = await ctx.awaitEvent(approvalEvent); - if (!approval.approved) { - return "rejected: " + approval.reason; - } - const choice = proposals[approval.choice]; - return choice; - }, -}); diff --git a/src/client/events.ts b/src/client/events.ts deleted file mode 100644 index bed12bf..0000000 --- a/src/client/events.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { EventId, EventSpec, VEventId } from "../types.js"; -import { v, type Infer, type Validator } from "convex/values"; - -/** - * Define a named event with a validator. - * @param spec - The event spec. - * @returns Utility functions to specify type-safe events and results. - */ -export function defineEvent< - Name extends string, - V extends Validator, ->(spec: { - name: Name; - validator?: V; -}): EventSpec> & { - /** - * A validator for the named event ID. - */ - vEventId: VEventId; - /** - * Use this to provide an ID to `awaitEvent` or `sendEvent`. - */ - withId: (id: EventId) => EventSpec>; -} { - return { - ...spec, - withId: (id: EventId) => ({ ...spec, id }), - vEventId: v.string() as VEventId, - }; -} - -export type TypedRunResult = - | { kind: "success"; returnValue: T } - | { kind: "failed"; error: string } - | { kind: "canceled" }; diff --git a/src/client/index.ts b/src/client/index.ts index 8fc00ec..1a69e2a 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -3,6 +3,7 @@ import type { WorkpoolOptions, WorkpoolRetryOptions, } from "@convex-dev/workpool"; +import { parse } from "convex-helpers/validators"; import { createFunctionHandle, type FunctionArgs, @@ -25,24 +26,22 @@ import type { import type { Step } from "../component/schema.js"; import type { EventId, - EventSpec, OnCompleteArgs, WorkflowId, WorkflowStep, } from "../types.js"; import { safeFunctionName } from "./safeFunctionName.js"; -import type { OpaqueIds, WorkflowComponent, WorkflowCtx } from "./types.js"; +import type { OpaqueIds, WorkflowComponent } from "./types.js"; +import type { WorkflowCtx } from "./workflowContext.js"; import { workflowMutation } from "./workflowMutation.js"; -import { parse } from "convex-helpers/validators"; export { vWorkflowId, - type WorkflowId, vWorkflowStep, + type WorkflowId, type WorkflowStep, } from "../types.js"; -export type { RunOptions, WorkflowCtx } from "./types.js"; -export { defineEvent } from "./events.js"; +export type { RunOptions, WorkflowCtx } from "./workflowContext.js"; export type CallbackOptions = { /** @@ -263,51 +262,93 @@ export class WorkflowManager { /** * Send an event to a workflow. * - * @param ctx - Either ctx from a mutation/action or a workflow step. - * @param args - The event arguments. + * @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: RunMutationCtx, - { - workflowId, - event, - value, - }: ( - | { workflowId: WorkflowId; event: EventSpec } - | { workflowId?: undefined; event: EventSpec & { id: string } } + args: ( + | { workflowId: WorkflowId; name: Name; id?: EventId } + | { workflowId?: undefined; name?: Name; id: EventId } ) & ( - | (T extends null - ? { value?: null; error?: undefined } - : { value: T; error?: undefined }) + | { validator?: undefined; value?: T } + | { validator: Validator; value: T } | { error: string; value?: undefined } ), ): Promise> { - let result = { - kind: "success" as const, - returnValue: event.validator ? parse(event.validator, value) : value, - } satisfies RunResult; + let result: RunResult = + "error" in args + ? { + kind: "failed", + error: args.error, + } + : { + kind: "success" as const, + returnValue: args.validator + ? parse(args.validator, args.value) + : "value" in args + ? args.value + : null, + }; return (await ctx.runMutation(this.component.event.send, { - eventId: event.id, + eventId: args.id, result, - name: event.name, - workflowId: workflowId, + name: args.name, + workflowId: args.workflowId, workpoolOptions: this.options?.workpoolOptions, })) as EventId; } + /** + * 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: RunMutationCtx, - component: WorkflowComponent, args: { name: Name; workflowId: WorkflowId }, ): Promise> { - return (await ctx.runMutation(component.event.create, { + return (await ctx.runMutation(this.component.event.create, { name: args.name, workflowId: args.workflowId, })) as EventId; } } +/** + * 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< + Name extends string, + V extends Validator, +>(spec: { name: Name; validator: V }) { + return spec; +} + type RunQueryCtx = { runQuery: GenericQueryCtx["runQuery"]; }; diff --git a/src/client/step.ts b/src/client/step.ts index 466bf12..9fa2a7e 100644 --- a/src/client/step.ts +++ b/src/client/step.ts @@ -17,9 +17,9 @@ import { journalEntrySize, valueSize, } from "../component/schema.js"; -import type { SchedulerOptions, WorkflowComponent } from "./types.js"; +import type { WorkflowComponent } from "./types.js"; import { MAX_JOURNAL_SIZE } from "../shared.js"; -import type { EventId } from "../types.js"; +import type { EventId, SchedulerOptions } from "../types.js"; export type WorkerResult = | { type: "handlerDone"; runResult: RunResult } diff --git a/src/client/stepContext.ts b/src/client/stepContext.ts deleted file mode 100644 index 867f6c7..0000000 --- a/src/client/stepContext.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { BaseChannel } from "async-channel"; -import type { - FunctionReference, - FunctionArgs, - FunctionReturnType, - FunctionType, -} from "convex/server"; -import { safeFunctionName } from "./safeFunctionName.js"; -import type { StepRequest } from "./step.js"; -import type { RetryOption } from "@convex-dev/workpool"; -import type { RunOptions, WorkflowCtx } from "./types.js"; -import type { EventSpec, WorkflowId } from "../types.js"; -import { parse } from "convex-helpers/validators"; - -export class StepContext implements WorkflowCtx { - constructor( - public workflowId: WorkflowId, - private sender: BaseChannel, - ) {} - - async runQuery>( - query: Query, - args: FunctionArgs, - opts?: RunOptions, - ): Promise> { - return this.runFunction("query", query, args, opts); - } - - async runMutation>( - mutation: Mutation, - args: FunctionArgs, - opts?: RunOptions, - ): Promise> { - return this.runFunction("mutation", mutation, args, opts); - } - - async runAction>( - action: Action, - args: FunctionArgs, - opts?: RunOptions & RetryOption, - ): Promise> { - return this.runFunction("action", action, args, opts); - } - - async runWorkflow>( - workflow: Workflow, - args: FunctionArgs, - opts?: RunOptions, - ): Promise> { - const { name, ...schedulerOptions } = opts ?? {}; - return this.run({ - name: name ?? safeFunctionName(workflow), - target: { - kind: "workflow", - function: workflow, - args, - }, - retry: undefined, - schedulerOptions, - }); - } - - async awaitEvent( - event: EventSpec, - ): Promise { - const result = await this.run({ - name: event.name, - target: { - kind: "event", - args: { eventId: event.id }, - }, - retry: undefined, - schedulerOptions: {}, - }); - if (event.validator) { - return parse(event.validator, result); - } - return result as T; - } - - private async runFunction< - F extends FunctionReference, - >( - functionType: FunctionType, - f: F, - args: unknown, - opts?: RunOptions & RetryOption, - ): Promise { - const { name, retry, ...schedulerOptions } = opts ?? {}; - return this.run({ - name: name ?? safeFunctionName(f), - target: { - kind: "function", - functionType, - function: f, - args, - }, - retry, - schedulerOptions, - }); - } - - private async run( - request: Omit, - ): Promise { - let send: unknown; - const p = new Promise((resolve, reject) => { - send = this.sender.push({ - ...request, - resolve, - reject, - }); - }); - await send; - return p; - } -} diff --git a/src/client/types.ts b/src/client/types.ts index 2427db1..f0f4279 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -1,116 +1,9 @@ -import type { RetryOption } from "@convex-dev/workpool"; -import type { - Expand, - FunctionArgs, - FunctionReference, - FunctionReturnType, -} from "convex/server"; +import type { Expand, FunctionReference } from "convex/server"; import type { api } from "../component/_generated/api.js"; import type { GenericId } from "convex/values"; -import type { EventSpec, WorkflowId } from "../types.js"; export type WorkflowComponent = UseApi; -export type RunOptions = { - /** - * The name of the function. By default, if you pass in api.foo.bar.baz, - * it will use "foo/bar:baz" as the name. If you pass in a function handle, - * it will use the function handle directly. - */ - name?: string; -} & SchedulerOptions; - -export type SchedulerOptions = - | { - /** - * The time (ms since epoch) to run the action at. - * If not provided, the action will be run as soon as possible. - * Note: this is advisory only. It may run later. - */ - runAt?: number; - } - | { - /** - * The number of milliseconds to run the action after. - * If not provided, the action will be run as soon as possible. - * Note: this is advisory only. It may run later. - */ - runAfter?: number; - }; - -export type WorkflowCtx = { - /** - * The ID of the workflow currently running. - */ - workflowId: WorkflowId; - /** - * Run a query with the given name and arguments. - * - * @param query - The query to run, like `internal.index.exampleQuery`. - * @param args - The arguments to the query function. - * @param opts - Options for scheduling and naming the query. - */ - runQuery>( - query: Query, - args: FunctionArgs, - opts?: RunOptions, - ): Promise>; - - /** - * Run a mutation with the given name and arguments. - * - * @param mutation - The mutation to run, like `internal.index.exampleMutation`. - * @param args - The arguments to the mutation function. - * @param opts - Options for scheduling and naming the mutation. - */ - runMutation>( - mutation: Mutation, - args: FunctionArgs, - opts?: RunOptions, - ): Promise>; - - /** - * Run an action with the given name and arguments. - * - * @param action - The action to run, like `internal.index.exampleAction`. - * @param args - The arguments to the action function. - * @param opts - Options for retrying, scheduling and naming the action. - */ - runAction>( - action: Action, - args: FunctionArgs, - opts?: RunOptions & RetryOption, - ): Promise>; - - /** - * Run a workflow with the given name and arguments. - * - * @param workflow - The workflow to run, like `internal.index.exampleWorkflow`. - * @param args - The arguments to the workflow function. - * @param opts - Options for retrying, scheduling and naming the workflow. - */ - runWorkflow>( - workflow: Workflow, - args: FunctionArgs["args"], - opts?: RunOptions, - ): Promise>; - - /** - * Blocks until a matching event is sent to this workflow. - * - * If an ID is specified, an event with that ID must already exist and must - * not already be "awaited" or "consumed". - * - * If a name is specified, the first available event is consumed that matches - * the name. If there is no available event, it will create one with that name - * with status "awaited". - * @param event - */ - awaitEvent( - event: EventSpec, - ): Promise; -}; - export type UseApi = Expand<{ [mod in keyof API]: API[mod] extends FunctionReference< infer FType, diff --git a/src/client/workflowContext.ts b/src/client/workflowContext.ts new file mode 100644 index 0000000..ea6a1aa --- /dev/null +++ b/src/client/workflowContext.ts @@ -0,0 +1,189 @@ +import type { RetryOption } from "@convex-dev/workpool"; +import { BaseChannel } from "async-channel"; +import { parse } from "convex-helpers/validators"; +import type { + FunctionArgs, + FunctionReference, + FunctionReturnType, + FunctionType, +} from "convex/server"; +import type { Validator } from "convex/values"; +import type { EventId, SchedulerOptions, WorkflowId } from "../types.js"; +import { safeFunctionName } from "./safeFunctionName.js"; +import type { StepRequest } from "./step.js"; + +export type RunOptions = { + /** + * The name of the function. By default, if you pass in api.foo.bar.baz, + * it will use "foo/bar:baz" as the name. If you pass in a function handle, + * it will use the function handle directly. + */ + name?: string; +} & SchedulerOptions; + +export type WorkflowCtx = { + /** + * The ID of the workflow currently running. + */ + workflowId: WorkflowId; + /** + * Run a query with the given name and arguments. + * + * @param query - The query to run, like `internal.index.exampleQuery`. + * @param args - The arguments to the query function. + * @param opts - Options for scheduling and naming the query. + */ + runQuery>( + query: Query, + args: FunctionArgs, + opts?: RunOptions, + ): Promise>; + + /** + * Run a mutation with the given name and arguments. + * + * @param mutation - The mutation to run, like `internal.index.exampleMutation`. + * @param args - The arguments to the mutation function. + * @param opts - Options for scheduling and naming the mutation. + */ + runMutation>( + mutation: Mutation, + args: FunctionArgs, + opts?: RunOptions, + ): Promise>; + + /** + * Run an action with the given name and arguments. + * + * @param action - The action to run, like `internal.index.exampleAction`. + * @param args - The arguments to the action function. + * @param opts - Options for retrying, scheduling and naming the action. + */ + runAction>( + action: Action, + args: FunctionArgs, + opts?: RunOptions & RetryOption, + ): Promise>; + + /** + * Run a workflow with the given name and arguments. + * + * @param workflow - The workflow to run, like `internal.index.exampleWorkflow`. + * @param args - The arguments to the workflow function. + * @param opts - Options for retrying, scheduling and naming the workflow. + */ + runWorkflow>( + workflow: Workflow, + args: FunctionArgs["args"], + opts?: RunOptions, + ): Promise>; + + /** + * Blocks until a matching event is sent to this workflow. + * + * If an ID is specified, an event with that ID must already exist and must + * not already be "awaited" or "consumed". + * + * If a name is specified, the first available event is consumed that matches + * the name. If there is no available event, it will create one with that name + * with status "awaited". + * @param event + */ + awaitEvent( + event: ( + | { name: Name; id?: EventId } + | { name?: Name; id: EventId } + ) & { + validator?: Validator; + }, + ): Promise; +}; + +export function createWorkflowCtx( + workflowId: WorkflowId, + sender: BaseChannel, +) { + return { + workflowId, + runQuery: async (query, args, opts?) => { + return runFunction(sender, "query", query, args, opts); + }, + + runMutation: async (mutation, args, opts?) => { + return runFunction(sender, "mutation", mutation, args, opts); + }, + + runAction: async (action, args, opts?) => { + return runFunction(sender, "action", action, args, opts); + }, + + runWorkflow: async (workflow, args, opts?) => { + const { name, ...schedulerOptions } = opts ?? {}; + return run(sender, { + name: name ?? safeFunctionName(workflow), + target: { + kind: "workflow", + function: workflow, + args, + }, + retry: undefined, + schedulerOptions, + }); + }, + + awaitEvent: async (event) => { + const result = await run(sender, { + name: event.name ?? event.id ?? "Event", + target: { + kind: "event", + args: { eventId: event.id }, + }, + retry: undefined, + schedulerOptions: {}, + }); + if (event.validator) { + return parse(event.validator, result); + } + return result as any; + }, + } satisfies WorkflowCtx; +} + +async function runFunction< + F extends FunctionReference, +>( + sender: BaseChannel, + functionType: FunctionType, + f: F, + args: unknown, + opts?: RunOptions & RetryOption, +): Promise { + const { name, retry, ...schedulerOptions } = opts ?? {}; + return run(sender, { + name: name ?? safeFunctionName(f), + target: { + kind: "function", + functionType, + function: f, + args, + }, + retry, + schedulerOptions, + }); +} + +async function run( + sender: BaseChannel, + request: Omit, +): Promise { + let send: unknown; + const p = new Promise((resolve, reject) => { + send = sender.push({ + ...request, + resolve, + reject, + }); + }); + await send; + return p; +} diff --git a/src/client/workflowMutation.ts b/src/client/workflowMutation.ts index 489aa1d..34b5136 100644 --- a/src/client/workflowMutation.ts +++ b/src/client/workflowMutation.ts @@ -18,7 +18,7 @@ import { type JournalEntry } from "../component/schema.js"; import { setupEnvironment } from "./environment.js"; import type { WorkflowDefinition } from "./index.js"; import { StepExecutor, type StepRequest, type WorkerResult } from "./step.js"; -import { StepContext } from "./stepContext.js"; +import { createWorkflowCtx } from "./workflowContext.js"; import { checkArgs } from "./validator.js"; import { type RunResult, type WorkpoolOptions } from "@convex-dev/workpool"; import { type WorkflowComponent } from "./types.js"; @@ -117,7 +117,7 @@ export function workflowMutation( const channel = new BaseChannel( workpoolOptions.maxParallelism ?? 10, ); - const step = new StepContext(workflowId, channel); + const step = createWorkflowCtx(workflowId, channel); const executor = new StepExecutor( workflowId, generationNumber, diff --git a/src/component/workflow.ts b/src/component/workflow.ts index 67284d3..3f896e0 100644 --- a/src/component/workflow.ts +++ b/src/component/workflow.ts @@ -24,10 +24,10 @@ import { type EventId, vPaginationResult, vWorkflowStep, + type SchedulerOptions, } from "../types.js"; import { api, internal } from "./_generated/api.js"; import { formatErrorWithStack } from "../shared.js"; -import type { SchedulerOptions } from "../client/types.js"; import type { Id } from "./_generated/dataModel.js"; import { paginator } from "convex-helpers/server/pagination"; diff --git a/src/types.ts b/src/types.ts index f8157e5..f7a7d6d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -64,6 +64,24 @@ export const vWorkflowStep = v.object({ // type assertion to keep us in check const _: Infer = {} as WorkflowStep; +export type SchedulerOptions = + | { + /** + * The time (ms since epoch) to run the action at. + * If not provided, the action will be run as soon as possible. + * Note: this is advisory only. It may run later. + */ + runAt?: number; + } + | { + /** + * The number of milliseconds to run the action after. + * If not provided, the action will be run as soon as possible. + * Note: this is advisory only. It may run later. + */ + runAfter?: number; + }; + export type OnCompleteArgs = { /** * The ID of the work that completed. From 89d14465f99a723582e09e8a9beee648001e4698 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Sat, 18 Oct 2025 17:23:30 -0700 Subject: [PATCH 45/52] readme --- README.md | 58 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a804d52..f6b4074 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ -# Convex Workflow +# Convex Durable Workflows [![npm version](https://badge.fury.io/js/@convex-dev%2Fworkflow.svg?)](https://badge.fury.io/js/@convex-dev%2Fworkflow) +The Workflow component enables you + Have you ever wanted to run a series of functions reliably and durably, where each can have its own retry behavior, the overall workflow will survive server restarts, and you can have long-running workflows spanning months that can be canceled? Do you want to observe the status of a workflow reactively, as well as the results written from each step? -And do you want to do this with code, instead of a DSL? +And do you want to do this with code, instead of a static configuration? Welcome to the world of Convex workflows. @@ -32,23 +34,39 @@ import { components } from "./_generated/api"; export const workflow = new WorkflowManager(components.workflow); -export const exampleWorkflow = workflow.define({ +export const userOnboarding = workflow.define({ args: { - storageId: v.id("_storage"), + userId: v.id("users"), }, - handler: async (step, args): Promise => { - const transcription = await step.runAction( - internal.index.computeTranscription, + handler: async (ctx, args): Promise => { + const status = await ctx.runMutation( + internal.emails.sendVerificationEmail, { storageId: args.storageId }, ); - const embedding = await step.runAction( - internal.index.computeEmbedding, - { transcription }, - // Run this a month after the transcription is computed. - { runAfter: 30 * 24 * 60 * 60 * 1000 }, + if (status === "needsVerification") { + // Waits until verification is completed asynchronously. + await ctx.awaitEvent({ name: "verificationEmail" }); + } + const result = await ctx.runAction( + internal.llm.generateCustomContent, + { userId: args.userId }, + // Retry this on transient errors with the default retry policy. + { retry: true }, + ); + if (result.needsHumanInput) { + // Run a whole workflow as a single step. + await ctx.runWorkflow(internal.llm.refineContentWorkflow, { + userId: args.userId, + }); + } + + await ctx.runMutation( + internal.emails.sendFollowUpEmailMaybe, + { userId: args.userId }, + // Runs one day after the previous step. + { runAfter: 24 * 60 * 60 * 1000 }, ); - return embedding; }, }); ``` @@ -97,9 +115,9 @@ is designed to feel like a Convex action but with a few restrictions: 1. The workflow runs in the background, so it can't return a value. 2. The workflow must be _deterministic_, so it should implement most of its logic - by calling out to other Convex functions. We will be lifting some of these - restrictions over time by implementing `Math.random()`, `Date.now()`, and - `fetch` within our workflow environment. + by calling out to other Convex functions. We restrict access to some + non-deterministic functions like `Math.random()` and `fetch`. Others we + patch, such as `console` for logging and `Date` for time. Note: To help avoid type cycles, always annotate the return type of the `handler` with the return type of the workflow. @@ -107,7 +125,9 @@ with the return type of the workflow. ```ts export const exampleWorkflow = workflow.define({ args: { name: v.string() }, + returns: v.string(), handler: async (step, args): Promise => { + // ^ Specify the return type of the handler const queryResult = await step.runQuery( internal.example.exampleQuery, args, @@ -283,11 +303,13 @@ export const exampleWorkflow = workflow.define({ }); ``` -### Specifying how many workflows can run in parallel +### Specifying step parallelism You can specify how many steps can run in parallel by setting the `maxParallelism` workpool option. It has a reasonable default. -On the free tier, you should not exceed 20. +On the free tier, you should not exceed 20, otherwise your other scheduled +functions may become delayed while competing for available functions with your +workflow steps. On a Pro account, you should not exceed 100 across all your workflows and workpools. If you want to do a lot of work in parallel, you should employ batching, where each workflow operates on a batch of work, e.g. scraping a list of links instead From fc352c830980495e2cd1adfba8e62e56b4eb2fb2 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Sat, 18 Oct 2025 17:24:29 -0700 Subject: [PATCH 46/52] 0.2.8-alpha.9 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 14635c7..04a6eec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.8", + "version": "0.2.8-alpha.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.8", + "version": "0.2.8-alpha.9", "license": "Apache-2.0", "dependencies": { "async-channel": "^0.2.0" diff --git a/package.json b/package.json index 0b08e11..46cc5b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@convex-dev/workflow", - "version": "0.2.8-alpha.8", + "version": "0.2.8-alpha.9", "description": "Convex component for durably executing workflows.", "keywords": [ "convex", From 5ccbea670814c2abb0639a9888a25fbd563df41e Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Mon, 20 Oct 2025 11:56:07 -0700 Subject: [PATCH 47/52] install exact --- package-lock.json | 55 ++++++++++++++++++++++++++++++++++++----------- package.json | 4 +++- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 04a6eec..10a7598 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "async-channel": "^0.2.0" }, "devDependencies": { - "@convex-dev/workpool": "^0.2.19", + "@convex-dev/workpool": "0.2.19", "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", @@ -20,7 +20,8 @@ "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", - "convex": "^1.27.3", + "convex": "1.28.0", + "convex-helpers": "0.1.102", "convex-test": "0.0.38", "cpy-cli": "6.0.0", "eslint": "9.37.0", @@ -132,6 +133,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -148,6 +150,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -164,6 +167,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -180,6 +184,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -196,6 +201,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -212,6 +218,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -228,6 +235,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -244,6 +252,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -260,6 +269,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -276,6 +286,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -292,6 +303,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -308,6 +320,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -324,6 +337,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -340,6 +354,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -356,6 +371,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -372,6 +388,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -388,6 +405,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -404,6 +422,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -420,6 +439,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -436,6 +456,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -452,6 +473,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -468,6 +490,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -484,6 +507,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -500,6 +524,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -516,6 +541,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2131,9 +2157,10 @@ "license": "MIT" }, "node_modules/convex": { - "version": "1.27.4", - "resolved": "https://registry.npmjs.org/convex/-/convex-1.27.4.tgz", - "integrity": "sha512-aPP3uxOF5v+K4uftXxRh8GAYepsjsFgU+S9IpAyLVNaFU3Z72WB1rIhaSzPAo4Q0TJWsOKANFGU903IU92QDTA==", + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.28.0.tgz", + "integrity": "sha512-40FgeJ/LxP9TxnkDDztU/A5gcGTdq1klcTT5mM0Ak+kSlQiDktMpjNX1TfkWLxXaE3lI4qvawKH95v2RiYgFxA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "esbuild": "0.25.4", @@ -2164,21 +2191,21 @@ } }, "node_modules/convex-helpers": { - "version": "0.1.99", - "resolved": "https://registry.npmjs.org/convex-helpers/-/convex-helpers-0.1.99.tgz", - "integrity": "sha512-W4sV9676vWWIwfYvG76Dxf7biDgpYggvwTLW5fJgLhXIb/XUCacO2AOXu+HrW85GvPRb1LLjhWgWPH8byHiTsw==", + "version": "0.1.102", + "resolved": "https://registry.npmjs.org/convex-helpers/-/convex-helpers-0.1.102.tgz", + "integrity": "sha512-FISEUHjTKZFk4GE2jZZt6AvTFZCrzoSW6aXJUTY5IIlfuFvJCry3i6YMvOC3lL6ivR75lVEsGt56rwGP2kCcNQ==", + "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "convex-helpers": "bin.cjs" }, "peerDependencies": { "@standard-schema/spec": "^1.0.0", - "convex": "^1.13.0", + "convex": "^1.24.0", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5", - "zod": "^3.22.4" + "zod": "^3.22.4 || ^4.0.15" }, "peerDependenciesMeta": { "@standard-schema/spec": { @@ -2359,6 +2386,7 @@ "version": "0.25.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -3563,6 +3591,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -4120,7 +4149,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -4679,7 +4708,7 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 46cc5b6..e011c02 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "async-channel": "^0.2.0" }, "devDependencies": { + "@convex-dev/workpool": "0.2.19", "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", @@ -67,7 +68,8 @@ "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", - "convex": "^1.27.3", + "convex": "1.28.0", + "convex-helpers": "0.1.102", "convex-test": "0.0.38", "cpy-cli": "6.0.0", "eslint": "9.37.0", From 414b3ed081c0e94538790b75e6a05177293c1af7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 02:13:15 +0000 Subject: [PATCH 48/52] Update dependency openai to v6.6.0 (#155) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 63ad9e3..bd456e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "eslint": "9.37.0", "globals": "16.4.0", "npm-run-all2": "8.0.4", - "openai": "6.5.0", + "openai": "6.6.0", "pkg-pr-new": "0.0.60", "prettier": "3.6.2", "typescript": "5.9.3", @@ -3268,9 +3268,9 @@ } }, "node_modules/openai": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.5.0.tgz", - "integrity": "sha512-bNqJ15Ijbs41KuJ2iYz/mGAruFHzQQt7zXo4EvjNLoB64aJdgn1jlMeDTsXjEg+idVYafg57QB/5Rd16oqvZ6A==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.6.0.tgz", + "integrity": "sha512-1yWk4cBsHF5Bq9TreHYOHY7pbqdlT74COnm8vPx7WKn36StS+Hyk8DdAitnLaw67a5Cudkz5EmlFQjSrNnrA2w==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index a278648..795a584 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "eslint": "9.37.0", "globals": "16.4.0", "npm-run-all2": "8.0.4", - "openai": "6.5.0", + "openai": "6.6.0", "pkg-pr-new": "0.0.60", "prettier": "3.6.2", "typescript": "5.9.3", From 160c2e5e5fa6023f37c54a343c20848dae0f51dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 05:55:19 +0000 Subject: [PATCH 49/52] Update dependency @types/node to v22.18.12 (#156) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd456e7..8a87254 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", - "@types/node": "22.18.11", + "@types/node": "22.18.12", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", @@ -1412,9 +1412,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.18.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.11.tgz", - "integrity": "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ==", + "version": "22.18.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz", + "integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==", "dev": true, "license": "MIT", "peer": true, diff --git a/package.json b/package.json index 795a584..d7f949b 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.37.0", - "@types/node": "22.18.11", + "@types/node": "22.18.12", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "chokidar-cli": "3.0.0", From 3b94e33202dc0ca5b2353774cf7f5a506c9e18cf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 08:48:42 +0000 Subject: [PATCH 50/52] Update eslint monorepo to v9.38.0 (#152) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 43 +++++++++++++++++++++---------------------- package.json | 4 ++-- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8a87254..d6e5438 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@convex-dev/workpool": "0.2.19", "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", - "@eslint/js": "9.37.0", + "@eslint/js": "9.38.0", "@types/node": "22.18.12", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", @@ -23,7 +23,7 @@ "convex": "1.28.0", "convex-test": "0.0.38", "cpy-cli": "6.0.0", - "eslint": "9.37.0", + "eslint": "9.38.0", "globals": "16.4.0", "npm-run-all2": "8.0.4", "openai": "6.6.0", @@ -567,13 +567,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -582,9 +582,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", - "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -645,9 +645,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", - "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", "dev": true, "license": "MIT", "engines": { @@ -658,9 +658,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2414,26 +2414,25 @@ } }, "node_modules/eslint": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", - "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.4.0", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.37.0", + "@eslint/js": "9.38.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", diff --git a/package.json b/package.json index d7f949b..f9f4c54 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@convex-dev/workpool": "0.2.19", "@edge-runtime/vm": "5.0.0", "@eslint/eslintrc": "3.3.1", - "@eslint/js": "9.37.0", + "@eslint/js": "9.38.0", "@types/node": "22.18.12", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", @@ -70,7 +70,7 @@ "convex": "1.28.0", "convex-test": "0.0.38", "cpy-cli": "6.0.0", - "eslint": "9.37.0", + "eslint": "9.38.0", "globals": "16.4.0", "npm-run-all2": "8.0.4", "openai": "6.6.0", From 4e03548cae2548e37fb118a141c59ab6b4b8d8c6 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Wed, 22 Oct 2025 15:17:57 -0700 Subject: [PATCH 51/52] renovate --- renovate.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/renovate.json b/renovate.json index 8e3387d..7abbc31 100644 --- a/renovate.json +++ b/renovate.json @@ -1,14 +1,23 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:best-practices"], + "schedule": ["* 0-4 * * 1"], + "timezone": "America/Los_Angeles", + "prConcurrentLimit": 1, "packageRules": [ { + "groupName": "Routine updates", "matchUpdateTypes": ["minor", "patch", "pin", "digest"], "automerge": true }, + { + "groupName": "Major updates", + "matchUpdateTypes": ["major"], + "automerge": false + }, { "matchDepTypes": ["devDependencies"], "automerge": true } - ], - "extends": ["config:best-practices"] + ] } From 88b6b332b832c183d50a7a1abfb8c3646a696e19 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Fri, 24 Oct 2025 22:54:39 -0700 Subject: [PATCH 52/52] fix short circuit --- src/component/journal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component/journal.ts b/src/component/journal.ts index ee68c34..62aeafd 100644 --- a/src/component/journal.ts +++ b/src/component/journal.ts @@ -54,7 +54,7 @@ export const load = query({ blocked: true, workflow, logLevel, - ok: false, + ok: true, }; } }