How It Works
In a human-in-the-loop workflow:- The workflow executes initial steps and emits progress events
- The workflow pauses at a specific point using
context.waitForEvent() - A “waiting for input” event is emitted to notify the frontend
- The user makes a decision in the frontend (approve/reject)
- The frontend calls an API to notify the workflow using
client.notify() - The workflow resumes with the user’s decision
- An “input resolved” event is emitted so the frontend can update its UI
- The workflow continues and completes based on the decision
Prerequisites
- An Upstash account with:
- A QStash project for workflows
- A Redis database for Realtime
- Next.js application set up
- Completed the basic real-time workflow setup
Event Types
For human-in-the-loop workflows, extend your schema inlib/realtime.ts with these additional event types:
waitingForInput: Emitted when the workflow pauses and needs user inputinputResolved: Emitted when the user provides input, so the frontend knows to clear the waiting state
Create the Realtime Middleware
Create a custom middleware that will emit events to Realtime atlib/middleware.ts:
lib/middleware.ts
- The middleware handles all realtime event emissions automatically
beforeExecution: Detects wait-for-event steps by checking the stepName and emitsworkflow.waitingForInputafterExecution: Emitsworkflow.inputResolvedfor wait steps andworkflow.stepFinishfor all stepsrunCompleted: Emitsworkflow.runFinishwhen the workflow finishes- All emission logic is centralized in the middleware, keeping workflow code clean
Building the Workflow
1. Create the Workflow Endpoint
Create your workflow atapp/api/workflow/human-in-loop/route.ts:
app/api/workflow/human-in-loop/route.ts
- Middleware for all events: The
realtimeMiddlewareautomatically handles all realtime event emissions by detecting wait-for-event steps through the stepName - Step name detection: The middleware checks if
stepName === "wait-for-approval"to know when to emitwaitingForInputandinputResolvedevents - Unique event IDs: Use a unique
eventId(likeapproval-${workflowRunId}) to identify which approval request this is - Timeout handling: Always handle the timeout case when waiting for events
2. Create the Notify Endpoint
Create an endpoint atapp/api/notify/route.ts to handle user input:
Building the Frontend
1. Extend the Custom Hook
Extend your hook from the basic example to handle waiting states:waitingState: Tracks when the workflow is waiting for inputcontinueWorkflow: Function to submit user decisions back to the workflow- Multiple events subscription: Uses
eventsarray to subscribe to multiple event types - Input resolved handling: Clears the waiting state when the workflow receives the user’s input
2. Use the Hook with Approval UI
How the Pattern Works
Timeline of Events
- Initial processing:
stepFinishevent → Frontend shows completed step - Waiting for approval:
waitingForInputevent → Frontend shows approval buttons - User clicks approve/reject: Frontend calls
/api/notify - Workflow resumes:
inputResolvedevent → Frontend hides approval buttons - Processing continues: More
stepFinishevents as workflow continues - Workflow completes:
runFinishevent → Frontend shows “Workflow Finished!”
Benefits
- Real-time feedback: Users see exactly when their approval is needed
- No polling: Instant updates via Server-Sent Events
- Timeout handling: Workflows don’t hang indefinitely waiting for input
Full Example
For a complete working example with all steps, error handling, and full UI components, check out the Upstash Realtime example on GitHub.Next Steps
- Review the basic real-time workflow pattern
- Learn about workflow event handling
- Explore Realtime features
- Check out workflow failure handling