From a5561d051778a857a4c30a0a54749b543918f7f6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 01:16:09 +0000 Subject: [PATCH 1/6] Add documentation for ctx.awaitEvent and ctx.runWorkflow features Co-Authored-By: Ian Macartney --- README.md | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/README.md b/README.md index 84f9903..5c0de07 100644 --- a/README.md +++ b/README.md @@ -415,6 +415,173 @@ export const exampleWorkflow = workflow.define({ }); ``` +### Awaiting events with `ctx.awaitEvent` + +Workflows can pause and wait for external events using `ctx.awaitEvent`. This is +useful for human-in-the-loop workflows, waiting for user confirmation, or +coordinating between different parts of your system. + +There are two ways to await events: by name (waits for the first available event +with that name) or by ID (waits for a specific pre-created event). + +#### Awaiting events by name + +The simplest approach is to await an event by name. The workflow will block +until an event with that name is sent to it. + +```ts +import { defineEvent, WorkflowManager } from "@convex-dev/workflow"; +import { v } from "convex/values"; + +// Define an event specification with a name and validator +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() }), + ), +}); + +export const confirmationWorkflow = workflow.define({ + args: { prompt: v.string() }, + returns: v.string(), + handler: async (ctx, args): Promise => { + const proposals = await ctx.runAction( + internal.example.generateProposals, + { prompt: args.prompt }, + ); + // Wait for user approval - blocks until the event is sent + const approval = await ctx.awaitEvent(approvalEvent); + if (!approval.approved) { + return "rejected: " + approval.reason; + } + return proposals[approval.choice]; + }, +}); +``` + +To send the event from elsewhere (e.g., after a user clicks a button): + +```ts +import { vWorkflowId, WorkflowManager } from "@convex-dev/workflow"; +import { v } from "convex/values"; + +export const approveWorkflow = mutation({ + args: { workflowId: vWorkflowId, choice: v.number() }, + handler: async (ctx, args) => { + await workflow.sendEvent(ctx, { + ...approvalEvent, + workflowId: args.workflowId, + value: { approved: true, choice: args.choice }, + }); + }, +}); +``` + +#### Awaiting events by ID + +For more control, you can create an event ahead of time and await it by ID. +This is useful when you need to pass the event ID to another system that will +later signal completion. + +```ts +import { type EventId, vEventId, vWorkflowId, WorkflowManager } from "@convex-dev/workflow"; + +export const signalWorkflow = workflow.define({ + args: {}, + handler: async (ctx) => { + for (let i = 0; i < 3; i++) { + // Create an event and get its ID + const signalId = await ctx.runMutation( + internal.example.createSignal, + { workflowId: ctx.workflowId }, + ); + // Wait for that specific event by ID + await ctx.awaitEvent({ id: signalId }); + console.log("Signal received", signalId); + } + }, +}); + +export const createSignal = internalMutation({ + args: { workflowId: vWorkflowId }, + handler: async (ctx, args): Promise => { + const eventId = await workflow.createEvent(ctx, { + name: "signal", + workflowId: args.workflowId, + }); + // Store or pass this eventId to another system + return eventId; + }, +}); + +export const sendSignal = internalMutation({ + args: { eventId: vEventId("signal") }, + handler: async (ctx, args) => { + await workflow.sendEvent(ctx, { id: args.eventId }); + }, +}); +``` + +#### Sending events with errors + +You can also send an event with an error, which will cause `ctx.awaitEvent` to +throw: + +```ts +await workflow.sendEvent(ctx, { + ...approvalEvent, + workflowId: args.workflowId, + error: "Request timed out", +}); +``` + +### Running nested workflows with `ctx.runWorkflow` + +You can run a workflow as a step within another workflow using `ctx.runWorkflow`. +The parent workflow will wait for the nested workflow to complete and receive +its return value. + +This is useful for composing complex workflows from simpler building blocks, +or for reusing workflow logic across different contexts. + +```ts +export const parentWorkflow = workflow.define({ + args: { prompt: v.string() }, + handler: async (ctx, args) => { + // Run a nested workflow and wait for its result + const length = await ctx.runWorkflow( + internal.example.childWorkflow, + { text: args.prompt }, + ); + console.log("Child workflow returned:", length); + + // Continue with more steps + await ctx.runMutation(internal.example.saveResult, { length }); + }, +}); + +export const childWorkflow = workflow.define({ + args: { text: v.string() }, + returns: v.number(), + handler: async (_ctx, args): Promise => { + return args.text.length; + }, +}); +``` + +The nested workflow runs as a single step in the parent workflow. If the nested +workflow fails, the parent workflow will also fail (unless you handle the error). +You can also specify scheduling options like `runAfter` or `runAt`: + +```ts +const result = await ctx.runWorkflow( + internal.example.childWorkflow, + { text: args.prompt }, + { runAfter: 5000 }, // Run after 5 seconds +); +``` + ## Tips and troubleshooting ### Circular dependencies From 3947c68dab426a110def3e92292beb1d4df43ca2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 01:19:04 +0000 Subject: [PATCH 2/6] Fix code snippets: add workflow variable and fix imports Co-Authored-By: Ian Macartney --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c0de07..ff046a2 100644 --- a/README.md +++ b/README.md @@ -432,6 +432,9 @@ until an event with that name is sent to it. ```ts import { defineEvent, WorkflowManager } from "@convex-dev/workflow"; import { v } from "convex/values"; +import { components, internal } from "./_generated/api"; + +const workflow = new WorkflowManager(components.workflow); // Define an event specification with a name and validator export const approvalEvent = defineEvent({ @@ -463,8 +466,11 @@ export const confirmationWorkflow = workflow.define({ To send the event from elsewhere (e.g., after a user clicks a button): ```ts -import { vWorkflowId, WorkflowManager } from "@convex-dev/workflow"; +import { vWorkflowId } from "@convex-dev/workflow"; import { v } from "convex/values"; +import { mutation } from "./_generated/server"; + +// assuming workflow and approvalEvent are defined as above export const approveWorkflow = mutation({ args: { workflowId: vWorkflowId, choice: v.number() }, @@ -486,6 +492,10 @@ later signal completion. ```ts import { type EventId, vEventId, vWorkflowId, WorkflowManager } from "@convex-dev/workflow"; +import { components, internal } from "./_generated/api"; +import { internalMutation } from "./_generated/server"; + +const workflow = new WorkflowManager(components.workflow); export const signalWorkflow = workflow.define({ args: {}, From 28cb955e750cad750dd86909cc5d6286929007fd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 01:21:43 +0000 Subject: [PATCH 3/6] Simplify ctx.awaitEvent/ctx.runWorkflow docs, link to examples Co-Authored-By: Ian Macartney --- README.md | 195 +++++++----------------------------------------------- 1 file changed, 25 insertions(+), 170 deletions(-) diff --git a/README.md b/README.md index ff046a2..c83678f 100644 --- a/README.md +++ b/README.md @@ -417,180 +417,35 @@ export const exampleWorkflow = workflow.define({ ### Awaiting events with `ctx.awaitEvent` -Workflows can pause and wait for external events using `ctx.awaitEvent`. This is -useful for human-in-the-loop workflows, waiting for user confirmation, or -coordinating between different parts of your system. - -There are two ways to await events: by name (waits for the first available event -with that name) or by ID (waits for a specific pre-created event). - -#### Awaiting events by name - -The simplest approach is to await an event by name. The workflow will block -until an event with that name is sent to it. - -```ts -import { defineEvent, WorkflowManager } from "@convex-dev/workflow"; -import { v } from "convex/values"; -import { components, internal } from "./_generated/api"; - -const workflow = new WorkflowManager(components.workflow); - -// Define an event specification with a name and validator -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() }), - ), -}); - -export const confirmationWorkflow = workflow.define({ - args: { prompt: v.string() }, - returns: v.string(), - handler: async (ctx, args): Promise => { - const proposals = await ctx.runAction( - internal.example.generateProposals, - { prompt: args.prompt }, - ); - // Wait for user approval - blocks until the event is sent - const approval = await ctx.awaitEvent(approvalEvent); - if (!approval.approved) { - return "rejected: " + approval.reason; - } - return proposals[approval.choice]; - }, -}); -``` - -To send the event from elsewhere (e.g., after a user clicks a button): - -```ts -import { vWorkflowId } from "@convex-dev/workflow"; -import { v } from "convex/values"; -import { mutation } from "./_generated/server"; - -// assuming workflow and approvalEvent are defined as above - -export const approveWorkflow = mutation({ - args: { workflowId: vWorkflowId, choice: v.number() }, - handler: async (ctx, args) => { - await workflow.sendEvent(ctx, { - ...approvalEvent, - workflowId: args.workflowId, - value: { approved: true, choice: args.choice }, - }); - }, -}); -``` - -#### Awaiting events by ID - -For more control, you can create an event ahead of time and await it by ID. -This is useful when you need to pass the event ID to another system that will -later signal completion. - -```ts -import { type EventId, vEventId, vWorkflowId, WorkflowManager } from "@convex-dev/workflow"; -import { components, internal } from "./_generated/api"; -import { internalMutation } from "./_generated/server"; - -const workflow = new WorkflowManager(components.workflow); - -export const signalWorkflow = workflow.define({ - args: {}, - handler: async (ctx) => { - for (let i = 0; i < 3; i++) { - // Create an event and get its ID - const signalId = await ctx.runMutation( - internal.example.createSignal, - { workflowId: ctx.workflowId }, - ); - // Wait for that specific event by ID - await ctx.awaitEvent({ id: signalId }); - console.log("Signal received", signalId); - } - }, -}); - -export const createSignal = internalMutation({ - args: { workflowId: vWorkflowId }, - handler: async (ctx, args): Promise => { - const eventId = await workflow.createEvent(ctx, { - name: "signal", - workflowId: args.workflowId, - }); - // Store or pass this eventId to another system - return eventId; - }, -}); - -export const sendSignal = internalMutation({ - args: { eventId: vEventId("signal") }, - handler: async (ctx, args) => { - await workflow.sendEvent(ctx, { id: args.eventId }); - }, -}); -``` - -#### Sending events with errors - -You can also send an event with an error, which will cause `ctx.awaitEvent` to -throw: - -```ts -await workflow.sendEvent(ctx, { - ...approvalEvent, - workflowId: args.workflowId, - error: "Request timed out", -}); -``` +Use `ctx.awaitEvent` inside a workflow handler to pause until an external event +is delivered. This is useful for human-in-the-loop flows or coordinating with +other systems. + +You can wait for events **by name** using `defineEvent` to create a typed event +specification: `const approval = await ctx.awaitEvent(approvalEvent);` where +`approvalEvent` is defined with `defineEvent({ name: "approval", validator })`. +From elsewhere, send the event with `workflow.sendEvent(ctx, { ...approvalEvent, workflowId, value })`. +See [`example/convex/userConfirmation.ts`](./example/convex/userConfirmation.ts) +for a complete example including the event validator and the mutation that sends +the approval. + +You can also wait for a **specific event by ID**: `await ctx.awaitEvent({ id: signalId });` +after creating an event with `workflow.createEvent(ctx, { name, workflowId })`. +See [`example/convex/passingSignals.ts`](./example/convex/passingSignals.ts) for +the complete pattern of creating, scheduling, and sending signals. + +To send an event that causes `ctx.awaitEvent` to throw an error, use +`workflow.sendEvent(ctx, { id, error: "error message" })`. ### Running nested workflows with `ctx.runWorkflow` -You can run a workflow as a step within another workflow using `ctx.runWorkflow`. -The parent workflow will wait for the nested workflow to complete and receive -its return value. - -This is useful for composing complex workflows from simpler building blocks, -or for reusing workflow logic across different contexts. - -```ts -export const parentWorkflow = workflow.define({ - args: { prompt: v.string() }, - handler: async (ctx, args) => { - // Run a nested workflow and wait for its result - const length = await ctx.runWorkflow( - internal.example.childWorkflow, - { text: args.prompt }, - ); - console.log("Child workflow returned:", length); - - // Continue with more steps - await ctx.runMutation(internal.example.saveResult, { length }); - }, -}); - -export const childWorkflow = workflow.define({ - args: { text: v.string() }, - returns: v.number(), - handler: async (_ctx, args): Promise => { - return args.text.length; - }, -}); -``` - -The nested workflow runs as a single step in the parent workflow. If the nested -workflow fails, the parent workflow will also fail (unless you handle the error). -You can also specify scheduling options like `runAfter` or `runAt`: +Use `ctx.runWorkflow` to run another workflow as a single step in the current +one. The parent workflow waits for the nested workflow to finish and receives +its return value: `const result = await ctx.runWorkflow(internal.example.childWorkflow, { args });` -```ts -const result = await ctx.runWorkflow( - internal.example.childWorkflow, - { text: args.prompt }, - { runAfter: 5000 }, // Run after 5 seconds -); -``` +You can also specify scheduling options like `{ runAfter: 5000 }` to delay the +nested workflow. See [`example/convex/nestedWorkflow.ts`](./example/convex/nestedWorkflow.ts) +for a complete parent/child workflow example. ## Tips and troubleshooting From 57801eb2e6c5f86822c5a3b7364e7cb90dc38fd8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 01:25:35 +0000 Subject: [PATCH 4/6] Restructure awaitEvent docs: basic usage first, then defineEvent Co-Authored-By: Ian Macartney --- README.md | 50 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c83678f..994aec5 100644 --- a/README.md +++ b/README.md @@ -421,21 +421,45 @@ Use `ctx.awaitEvent` inside a workflow handler to pause until an external event is delivered. This is useful for human-in-the-loop flows or coordinating with other systems. -You can wait for events **by name** using `defineEvent` to create a typed event -specification: `const approval = await ctx.awaitEvent(approvalEvent);` where -`approvalEvent` is defined with `defineEvent({ name: "approval", validator })`. -From elsewhere, send the event with `workflow.sendEvent(ctx, { ...approvalEvent, workflowId, value })`. -See [`example/convex/userConfirmation.ts`](./example/convex/userConfirmation.ts) -for a complete example including the event validator and the mutation that sends -the approval. +At its simplest, you can wait for an event **by name**: + +```ts +await ctx.awaitEvent({ name: "approval" }); +``` + +or for a **specific event by ID**: + +```ts +await ctx.awaitEvent({ id: signalId }); +``` + +To unblock a waiting workflow, call `workflow.sendEvent` from a mutation or +action. You can send a value with the event, or send an error that will cause +`ctx.awaitEvent` to throw. See +[`example/convex/passingSignals.ts`](./example/convex/passingSignals.ts) for a +complete example of creating events, passing their IDs around, and sending +signals. -You can also wait for a **specific event by ID**: `await ctx.awaitEvent({ id: signalId });` -after creating an event with `workflow.createEvent(ctx, { name, workflowId })`. -See [`example/convex/passingSignals.ts`](./example/convex/passingSignals.ts) for -the complete pattern of creating, scheduling, and sending signals. +#### Sharing event definitions with `defineEvent` -To send an event that causes `ctx.awaitEvent` to throw an error, use -`workflow.sendEvent(ctx, { id, error: "error message" })`. +Use `defineEvent` to define an event's name and validator in one place, then +share it between the workflow and the sender: + +```ts +const approvalEvent = defineEvent({ + name: "approval", + validator: v.object({ approved: v.boolean() }), +}); + +// In the workflow: +const approval = await ctx.awaitEvent(approvalEvent); + +// From a mutation: +await workflow.sendEvent(ctx, { ...approvalEvent, workflowId, value }); +``` + +See [`example/convex/userConfirmation.ts`](./example/convex/userConfirmation.ts) +for a full approval flow built this way. ### Running nested workflows with `ctx.runWorkflow` From 802c90b8197e104743b3677a503b0a23488317d7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 18:15:58 +0000 Subject: [PATCH 5/6] Show actual value in sendEvent example Co-Authored-By: Ian Macartney --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 994aec5..d1f0568 100644 --- a/README.md +++ b/README.md @@ -455,7 +455,7 @@ const approvalEvent = defineEvent({ const approval = await ctx.awaitEvent(approvalEvent); // From a mutation: -await workflow.sendEvent(ctx, { ...approvalEvent, workflowId, value }); +await workflow.sendEvent(ctx, { ...approvalEvent, workflowId, value: { approved: true } }); ``` See [`example/convex/userConfirmation.ts`](./example/convex/userConfirmation.ts) From 48b1dabf3e1699f5a57138efda9aa7641cdae39a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 19:31:36 +0000 Subject: [PATCH 6/6] Add 'as const' to defineEvent example for type safety Co-Authored-By: Ian Macartney --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d1f0568..2b3e0ef 100644 --- a/README.md +++ b/README.md @@ -447,7 +447,7 @@ share it between the workflow and the sender: ```ts const approvalEvent = defineEvent({ - name: "approval", + name: "approval" as const, validator: v.object({ approved: v.boolean() }), });