get-convex / get-convex/workflow
Feature Request: Simplified external webhook/callback processing
- Dominant language
- TypeScript
- Stars
- 81
- Forks
- 17
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 4
Description
## Summary
Add a built-in mechanism for workflows to pause and wait for external HTTP callbacks without requiring developers to manually create HTTP actions, parse webhooks, and wire up `sendEvent()` calls.
## The Problem
When integrating with async 3rd-party APIs that use webhook callbacks (Replicate, Fal.ai, RunPod, Modal, Resend, Stripe, etc.), the current Convex Workflow implementation requires significant boilerplate:
1. Define an event with `defineEvent()`
2. Create an HTTP action to receive the webhook
3. Parse the incoming webhook payload
4. Call an internal mutation that invokes `workflow.sendEvent()`
5. Manually construct the webhook URL with the workflowId as a query param
6. Register the HTTP route
This is a lot of ceremony for what is conceptually a simple operation: "call this API, wait for it to call me back."
---
## Proposed Solution
Introduce a `ctx.createCallbackToken()` API (or similar) that returns a pre-built, secure callback URL. When the external service POSTs to this URL, the workflow automatically resumes with the payload.
**Proposed API (~15 lines, single file):**
```typescript
export const generateImageWorkflow = workflow.define({
args: { prompt: v.string() },
returns: v.object({ imageUrl: v.string() }),
handler: async (ctx, args): Promise<{ imageUrl: string }> => {
// Step 1: Create a callback token (deterministic, no side effects)
// Returns a secure URL that Convex hosts and routes automatically
const callbackToken = await ctx.createCallbackToken({
timeout: "5m",
});
// Step 2: Pass the callback URL to an action that calls the external API
const jobId = await ctx.runAction(internal.example.startImageGeneration, {
prompt: args.prompt,
callbackUrl: callbackToken.url, // e.g., https://callbacks.convex.cloud/wf/{id}/{token}
});
// Step 3: Wait for the external service to POST to the callback URL
// Convex handles the HTTP endpoint internally and resumes the workflow
const result = await ctx.awaitCallback(callbackToken);
if (result.status === "failed") {
throw new Error(result.error);
}
return { imageUrl: result.output[0] };
},
});
// Standard action - nothing special here, just uses the provided URL
export const startImageGeneration = internalAction({
args: { prompt: v.string(), callbackUrl: v.string() },
returns: v.string(),
handler: async (_ctx, args) => {
const response = await fetch("https://api.replicate.com/v1/predictions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.REPLICATE_API_TOKEN}`,
},
body: JSON.stringify({
version: "stability-ai/sdxl:abc123...",
input: { prompt: args.prompt },
webhook: args.callbackUrl, // Just pass it through
webhook_events_filter: ["completed"],
}),
});
const job = await response.json();
return job.id;
},
});
```
### Implementation Notes
- The callback URL would be hosted by Convex (e.g., `https://callbacks.convex.cloud/wf/{workflowId}/{tokenId}`)
- Convex would handle the HTTP endpoint, validation, and automatic `sendEvent()` internally
- The JSON body of the POST request becomes the callback payload
- Tokens would be single-use and expire after timeout
- Could support optional payload validation via a validator argument
---
## Benefits
1. **Massive DX improvement** - 75% less code for a very common integration pattern
2. **Fewer moving parts** - No custom HTTP actions, no manual URL construction, no separate event definitions
3. **Less error-prone** - No risk of forgetting to register HTTP routes or mismatching event names
4. **Batteries-included** - Timeout handling, idempotency, and security built in
5. **Competitive parity** - Trigger.dev's `wait.createToken()` / `wait.forToken()` pattern is highly praised and this would match that ergonomics
---
## Use Cases
This pattern is extremely common when integrating with:
- **AI/ML APIs**: Replicate, Fal.ai, RunPod, Modal, Hugging Face Inference Endpoints
- **Media processing**: Mux, Cloudinary, Transloadit
- **Payments**: Stripe (async payment confirmations), PayPal
- **Document processing**: DocuSign, PDF services
- **Any long-running async API** that supports webhooks
---
## References
- [[Trigger.dev Wait for Token](https://trigger.dev/docs/wait-for-token)](https://trigger.dev/docs/wait-for-token) - The gold standard for this pattern
- Current Convex `awaitEvent` + `sendEvent` approach works but requires too much scaffolding
Would love to hear the team's thoughts on this. Happy to help test if you build it!
Contributor guide
Assessment
This issue has not been assessed yet.