Real world web application to implement human-in-loop(interrupt) in langgraph4j
Excellent question! This is where the real-world architecture matters. Let me show you the correct pattern:
❌ WRONG Approach (Don’t Do This):
// BAD: Storing workflow in session/global variable
HttpSession session = request.getSession();
session.setAttribute("workflow", app); // ❌ NO!
✅ CORRECT Approach:
The key insight: The workflow object is just the definition (stateless). The checkpoint stores the state (in database/memory).
[1 tool called]
Perfect! Here’s the real-world pattern:
🌐 Web Application Architecture:
✅ CORRECT Pattern:
┌─────────────────────────────────────────────────────┐
│ HTTP Request 1: Submit │
│ POST /api/requests │
├─────────────────────────────────────────────────────┤
│ 1. Generate request_id = "REQ-123" │
│ 2. Create workflow (fresh instance) │
│ 3. Execute: workflow.stream(data, config) │
│ 4. Checkpoint auto-saved │
│ 5. Return: { request_id, status, message } │
└─────────────────────────────────────────────────────┘
↓
(User closes browser, time passes...)
↓
┌─────────────────────────────────────────────────────┐
│ HTTP Request 2: Check Status │
│ GET /api/requests/REQ-123 │
├─────────────────────────────────────────────────────┤
│ 1. Create workflow (NEW instance!) │
│ 2. Load checkpoint: workflow.getState(config) │
│ 3. Show status to user │
│ 4. Return: { status, actions: ["approve"] } │
└─────────────────────────────────────────────────────┘
↓
(User reviews and approves...)
↓
┌─────────────────────────────────────────────────────┐
│ HTTP Request 3: Approve │
│ POST /api/requests/REQ-123/approve │
├─────────────────────────────────────────────────────┤
│ 1. Create workflow (NEW instance again!) │
│ 2. Load checkpoint │
│ 3. Update: workflow.updateState(...) │
│ 4. Resume: workflow.stream(null, config) │
│ 5. Return: { status: "completed" } │
└─────────────────────────────────────────────────────┘
🔑 Key Points:
1. Store Only thread_id
// In your database/session
{
"request_id": "REQ-123", // ← Store this
"user_id": "user_456",
"created_at": "2024-01-15"
}
// DON'T store workflow object!
2. Recreate Workflow on Each Request
public Map<String, Object> handleRequest(String requestId) {
// Fresh workflow instance each time - it's cheap!
CompiledGraph<AgentState> workflow = buildWorkflow();
RunnableConfig config = RunnableConfig.builder()
.threadId(requestId) // Links to checkpoint
.build();
// Load state from checkpoint
StateSnapshot<AgentState> snapshot = workflow.getState(config);
// ...
}
3. Return State to UI
// Get current state
AgentState state = snapshot.state();
// Return to UI
return Map.of(
"status", state.data().get("status"),
"amount", state.data().get("amount"),
"needs_approval", true,
"actions", List.of("approve", "reject")
);
4. UI Shows Approval Form
// Frontend receives:
{
"request_id": "REQ-123",
"status": "pending_approval",
"amount": 5000,
"actions": ["approve", "reject"]
}
// UI renders:
// [ Approve ] [ Reject ] buttons
5. User Approves → Resume
// User clicks "Approve"
// Frontend sends: POST /api/requests/REQ-123/approve
// Backend:
workflow.updateState(config, approvalData, "review");
workflow.stream(null, config); // Resume!
💾 Production: Use Database Checkpoint
// Replace MemorySaver with database persistence
CompileConfig config = CompileConfig.builder()
.checkpointSaver(new PostgresCheckpointSaver(dataSource))
// or .checkpointSaver(new MongoCheckpointSaver(mongoClient))
.build();
🎯 Why This Works:
| Component | What It Stores | Lifetime |
|---|---|---|
| Workflow | Definition/logic | Recreated each request |
| Checkpoint | State/data | Persistent (database) |
| thread_id | Link to checkpoint | Stored in DB/session |
🚀 Run the Example:
java ai.RealWorldWorkflowPattern
This shows 3 separate HTTP requests simulating a real web application! Each request creates a fresh workflow but loads the same checkpoint state. Perfect for production! 🎉
package ai;
import org.bsc.langgraph4j.*;
import org.bsc.langgraph4j.checkpoint.MemorySaver;
import org.bsc.langgraph4j.state.AgentState;
import org.bsc.langgraph4j.state.StateSnapshot;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import static org.bsc.langgraph4j.StateGraph.END;
import static org.bsc.langgraph4j.StateGraph.START;
/**
* Real-World Pattern: How to Use LangGraph in Web Applications
*
* Key Points:
* - Don't store workflow in session/global variable
* - Workflow is stateless, can be recreated
* - Only store thread_id (request ID)
* - Checkpoint stores the state (in database)
* - Recreate workflow on each HTTP request
*/
public class RealWorldWorkflowPattern {
// ================================================================
// WORKFLOW DEFINITION (Stateless - can be recreated anytime)
// ================================================================
private static Map<String, Object> reviewRequest(AgentState state) {
System.out.println("[REVIEW] Request under review");
Map<String, Object> updates = new HashMap<>();
updates.put("status", "pending_approval");
updates.put("current_stage", "review");
return updates;
}
private static Map<String, Object> approveRequest(AgentState state) {
System.out.println("[APPROVE] Request approved");
Map<String, Object> updates = new HashMap<>();
updates.put("status", "approved");
updates.put("current_stage", "approved");
return updates;
}
private static Map<String, Object> executeRequest(AgentState state) {
System.out.println("[EXECUTE] Executing request");
Map<String, Object> updates = new HashMap<>();
updates.put("status", "completed");
updates.put("current_stage", "completed");
return updates;
}
/**
* Build workflow - This can be called multiple times
* It's just the definition, not the state!
*/
public static CompiledGraph<AgentState> buildWorkflow() throws Exception {
StateGraph<AgentState> workflow = new StateGraph<>(AgentState::new);
workflow.addNode("review", state -> CompletableFuture.completedFuture(reviewRequest(state)));
workflow.addNode("approve", state -> CompletableFuture.completedFuture(approveRequest(state)));
workflow.addNode("execute", state -> CompletableFuture.completedFuture(executeRequest(state)));
workflow.addEdge(START, "review");
workflow.addEdge("review", "approve");
workflow.addEdge("approve", "execute");
workflow.addEdge("execute", END);
// Use MemorySaver (in production, use persistent storage!)
CompileConfig config = CompileConfig.builder()
.checkpointSaver(new MemorySaver()) // TODO: Replace with DB in production
.interruptAfter("review")
.build();
return workflow.compile(config);
}
// ================================================================
// SIMULATED WEB APPLICATION PATTERN
// ================================================================
/**
* REQUEST 1: User submits a new approval request
*
* HTTP POST /api/requests
* Body: { "amount": 5000, "description": "..." }
*/
public static Map<String, Object> handleSubmitRequest(Map<String, Object> requestData) {
try {
System.out.println("\n" + "=".repeat(70));
System.out.println("HTTP REQUEST 1: POST /api/requests");
System.out.println("=".repeat(70));
// 1. Generate unique request ID
String requestId = "REQ-" + System.currentTimeMillis();
System.out.println("Generated Request ID: " + requestId);
// 2. Store basic info in database (metadata)
// database.save(requestId, requestData);
System.out.println("Saved to database: " + requestId);
// 3. Create workflow (fresh instance - no session storage!)
CompiledGraph<AgentState> workflow = buildWorkflow();
// 4. Create config with thread_id
RunnableConfig config = RunnableConfig.builder()
.threadId(requestId) // This is what links to checkpoint
.build();
// 5. Start workflow execution
System.out.println("\nStarting workflow...");
workflow.stream(requestData, config).forEach(output -> {
System.out.println(" → " + output.node());
});
// 6. Get current state from checkpoint
StateSnapshot<AgentState> snapshot = workflow.getState(config);
AgentState state = snapshot.state();
// 7. Build response for UI
Map<String, Object> response = new HashMap<>();
response.put("request_id", requestId);
response.put("status", state.data().get("status"));
response.put("current_stage", state.data().get("current_stage"));
response.put("message", "Request submitted. Awaiting approval.");
response.put("amount", state.data().get("amount"));
System.out.println("\nResponse to UI: " + response);
return response;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* REQUEST 2: Get current status of a request
*
* HTTP GET /api/requests/{requestId}
*/
public static Map<String, Object> handleGetRequestStatus(String requestId) {
try {
System.out.println("\n" + "=".repeat(70));
System.out.println("HTTP REQUEST 2: GET /api/requests/" + requestId);
System.out.println("=".repeat(70));
// 1. Recreate workflow (it's just the definition!)
CompiledGraph<AgentState> workflow = buildWorkflow();
// 2. Create config with same thread_id
RunnableConfig config = RunnableConfig.builder()
.threadId(requestId)
.build();
// 3. Get state from checkpoint
StateSnapshot<AgentState> snapshot = workflow.getState(config);
AgentState state = snapshot.state();
// 4. Build response for UI
Map<String, Object> response = new HashMap<>();
response.put("request_id", requestId);
response.put("status", state.data().get("status"));
response.put("current_stage", state.data().get("current_stage"));
response.put("amount", state.data().get("amount"));
response.put("needs_approval", "pending_approval".equals(state.data().get("status")));
// What can user do?
String currentStage = (String) state.data().get("current_stage");
if ("review".equals(currentStage)) {
response.put("actions", List.of("approve", "reject"));
response.put("message", "Please review and approve/reject this request");
}
System.out.println("Response to UI: " + response);
return response;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* REQUEST 3: User approves the request
*
* HTTP POST /api/requests/{requestId}/approve
* Body: { "approved": true, "comments": "..." }
*/
public static Map<String, Object> handleApproveRequest(String requestId, Map<String, Object> approvalData) {
try {
System.out.println("\n" + "=".repeat(70));
System.out.println("HTTP REQUEST 3: POST /api/requests/" + requestId + "/approve");
System.out.println("=".repeat(70));
// 1. Recreate workflow
CompiledGraph<AgentState> workflow = buildWorkflow();
// 2. Create config with same thread_id
RunnableConfig config = RunnableConfig.builder()
.threadId(requestId)
.build();
// 3. Get current state to know which stage we're at
StateSnapshot<AgentState> snapshot = workflow.getState(config);
String currentStage = (String) snapshot.state().data().get("current_stage");
System.out.println("Current stage: " + currentStage);
System.out.println("Applying approval: " + approvalData);
// 4. Update checkpoint with approval
Map<String, Object> updates = new HashMap<>();
updates.put("approved", approvalData.get("approved"));
updates.put("approver", approvalData.get("approver"));
updates.put("comments", approvalData.get("comments"));
updates.put("approved_at", System.currentTimeMillis());
workflow.updateState(config, updates, currentStage);
// 5. Resume workflow
System.out.println("\nResuming workflow...");
workflow.stream(null, config).forEach(output -> {
System.out.println(" → " + output.node());
});
// 6. Get updated state
StateSnapshot<AgentState> updatedSnapshot = workflow.getState(config);
AgentState updatedState = updatedSnapshot.state();
// 7. Build response
Map<String, Object> response = new HashMap<>();
response.put("request_id", requestId);
response.put("status", updatedState.data().get("status"));
response.put("current_stage", updatedState.data().get("current_stage"));
response.put("message", "Request approved and processed successfully");
System.out.println("\nResponse to UI: " + response);
return response;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// ================================================================
// MAIN: Simulates the complete flow
// ================================================================
public static void main(String[] args) {
System.out.println("╔════════════════════════════════════════════════════════════╗");
System.out.println("║ Real-World Web Application Pattern ║");
System.out.println("╚════════════════════════════════════════════════════════════╝");
try {
// ================================================================
// SCENARIO: User interacts with web application
// ================================================================
// Request 1: User submits request via web form
Map<String, Object> requestData = new HashMap<>();
requestData.put("amount", 5000);
requestData.put("description", "New equipment");
Map<String, Object> submitResponse = handleSubmitRequest(requestData);
String requestId = (String) submitResponse.get("request_id");
System.out.println("\n💡 UI shows: Request submitted! ID = " + requestId);
// Simulate time passing (user closes browser, goes for coffee...)
Thread.sleep(2000);
// Request 2: User comes back, checks status
Map<String, Object> statusResponse = handleGetRequestStatus(requestId);
System.out.println("\n💡 UI shows: Status = " + statusResponse.get("status"));
System.out.println("💡 UI shows approval form with buttons: Approve / Reject");
// Simulate time passing
Thread.sleep(2000);
// Request 3: User approves
Map<String, Object> approvalData = new HashMap<>();
approvalData.put("approved", true);
approvalData.put("approver", "Manager John");
approvalData.put("comments", "Looks good!");
Map<String, Object> approveResponse = handleApproveRequest(requestId, approvalData);
System.out.println("\n💡 UI shows: " + approveResponse.get("message"));
System.out.println("💡 Final status: " + approveResponse.get("status"));
// ================================================================
// KEY POINTS SUMMARY
// ================================================================
System.out.println("\n\n" + "=".repeat(70));
System.out.println("KEY ARCHITECTURAL PATTERNS");
System.out.println("=".repeat(70));
System.out.println("\n✅ DO:");
System.out.println(" 1. Store only thread_id (request ID) in session/database");
System.out.println(" 2. Recreate workflow object on each HTTP request");
System.out.println(" 3. Use thread_id to load checkpoint state");
System.out.println(" 4. Return current state to UI for display");
System.out.println(" 5. Use checkpoint for persistence (survives restarts)");
System.out.println("\n❌ DON'T:");
System.out.println(" 1. Store workflow object in session");
System.out.println(" 2. Store workflow in global/static variable");
System.out.println(" 3. Try to keep workflow in memory");
System.out.println(" 4. Worry about recreating workflow (it's cheap!)");
System.out.println("\n📊 DATA FLOW:");
System.out.println(" Request 1 (Submit):");
System.out.println(" UI → Server → Create Workflow → Execute → Save Checkpoint");
System.out.println(" → Return (request_id, status) → UI");
System.out.println("\n Request 2 (Check Status):");
System.out.println(" UI → Server → Create Workflow → Load Checkpoint");
System.out.println(" → Return (status, actions) → UI");
System.out.println("\n Request 3 (Approve):");
System.out.println(" UI → Server → Create Workflow → Load Checkpoint");
System.out.println(" → Update Checkpoint → Resume → Save Checkpoint");
System.out.println(" → Return (new status) → UI");
System.out.println("\n🔑 THE MAGIC:");
System.out.println(" • Workflow = Definition (stateless, recreatable)");
System.out.println(" • Checkpoint = State (persistent, survives restarts)");
System.out.println(" • thread_id = Link between requests");
System.out.println(" • Each HTTP request is independent!");
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
}
}
}