--- title: Basic Workflow Patterns description: Learn common patterns and techniques for building effective workflows --- This guide explores common patterns you can use to build more complex workflows with workflows. ## Fan-out (Parallelism) One of the most powerful features of workflows is the ability to run tasks in parallel: ```ts import { createWorkflow, workflowEvent, getContext, } from "@llamaindex/workflow-core"; // Define events const startEvent = workflowEvent(); const processItemEvent = workflowEvent(); const resultEvent = workflowEvent(); const completeEvent = workflowEvent(); // Create workflow const workflow = createWorkflow(); // Define a variable accessible within the handler scope to signal completion let itemsToProcess = 10; // Total number of items let itemsProcessed = 0; // Process start event: fan out to multiple processItemEvent events workflow.handle([startEvent], async (start) => { const { sendEvent, stream } = getContext(); itemsProcessed = 0; // Reset counter for this execution context // Emit multiple events to be processed in parallel for (let i = 0; i < itemsToProcess; i++) { sendEvent(processItemEvent.with(i)); } // Use an async IIFE to collect results and emit completeEvent try { const results = await stream .filter(resultEvent) .until(() => itemsProcessed === itemsToProcess) .toArray(); // Send the final aggregated result sendEvent(completeEvent.with(results.map((event) => event.data))); } catch (err) { console.error("Error processing items:", err); // Handle error if needed } // Note: This handler finishes *before* the collection completes. // Returning nothing or a specific "processing started" event might be appropriate. }); // Process each item workflow.handle([processItemEvent], async (event) => { // Simulate async work await new Promise((resolve) => setTimeout(resolve, Math.random() * 100)); const processedValue = `Processed: ${event.data}`; // Crucially, update the shared counter *after* processing itemsProcessed++; return resultEvent.with(processedValue); }); // Example E2E Run Usage async function runFanOut() { console.log("Running fan-out"); const { stream, sendEvent } = workflow.createContext(); sendEvent(startEvent.with("Start fan-out")); for await (const event of stream) { if (processItemEvent.include(event)) { console.log(`Processing item: ${event.data}`); } else if (resultEvent.include(event)) { console.log(`Result received: ${event.data}`); } else if (completeEvent.include(event)) { console.log("Final aggregated results:", event.data); break; // Stop processing the stream } } } runFanOut(); ``` This pattern allows you to: 1. Emit multiple events to be processed in parallel 2. Collect results as they come in 3. Complete once all parallel tasks are finished ## Conditional Branching You can implement conditional logic in your workflows: ```ts import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core"; const inputEvent = workflowEvent(); const evenNumberEvent = workflowEvent(); const oddNumberEvent = workflowEvent(); const resultEvent = workflowEvent(); const workflow = createWorkflow(); // Branch based on whether the number is even or odd workflow.handle([inputEvent], (event) => { if (event.data % 2 === 0) { return evenNumberEvent.with(`${event.data} is even`); } else { return oddNumberEvent.with(`${event.data} is odd`); } }); // Handle even numbers workflow.handle([evenNumberEvent], (event) => { return resultEvent.with(`Even result: ${event.data}`); }); // Handle odd numbers workflow.handle([oddNumberEvent], (event) => { return resultEvent.with(`Odd result: ${event.data}`); }); // Example E2E Run Usage async function run(input_number: number) { // Create a workflow context and send the initial event const { stream, sendEvent } = workflow.createContext(); sendEvent(inputEvent.with(input_number)); // Collect all events until we get a stopEvent const allEvents = await stream.until(resultEvent).toArray(); // The last event will be the stopEvent that was requested const finalEvent = allEvents[allEvents.length - 1]; if (resultEvent.include(finalEvent)) { console.log(`Result: ${finalEvent.data}`); } } run(42); run(43); ``` ## Using Middleware LlamaIndex workflows provide middleware that can enhance your workflows: ### `withState` Middleware The `withState` middleware adds a persistent state to your workflow context: ```ts import { createStatefulMiddleware } from "@llamaindex/workflow-core/middleware/state"; // Define your state type type CounterState = { count: number; history: number[]; }; // Create a workflow with state middleware const { withState } = createStatefulMiddleware(() => ({ count: 0, history: [], })); const workflow = withState(createWorkflow()); // Use the state in your handlers workflow.handle([startEvent], () => { const { state } = getContext(); state.count += 1; state.history.push(state.count); return incrementEvent.with(state.count); }); workflow.handle([incrementEvent], (increment) => { const { state } = getContext(); console.log("Current count:", state.count); console.log("History:", state.history); return resultEvent.with(state.count); }); // Run the workflow async function runWithState() { const { sendEvent, state } = workflow.createContext(); // Send start event multiple times to see state update sendEvent(startEvent.with()); sendEvent(startEvent.with()); sendEvent(startEvent.with()); // The state persists across events console.log("Final count:", state.count); console.log("Final history:", state.history); } runWithState(); ``` You can also create a state with input: ```ts const { withState } = createStatefulMiddleware( (input: { initialCount: number }) => ({ count: input.initialCount, history: [input.initialCount], }), ); const workflow = withState(createWorkflow()); const { state } = workflow.createContext({ initialCount: 10 }); ``` ### `withValidation` Middleware The `withValidation` middleware adds compile-time and runtime validation to your workflows: ```ts import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core"; import { withValidation } from "@llamaindex/workflow-core/middleware/validation"; const startEvent = workflowEvent(); const processEvent = workflowEvent(); const resultEvent = workflowEvent(); const disallowedEvent = workflowEvent(); // Create a workflow with validation middleware // Define allowed event paths const workflow = withValidation(createWorkflow(), [ [[startEvent], [processEvent]], // startEvent can only lead to processEvent [[processEvent], [resultEvent]], // processEvent can only lead to resultEvent ]); // This will pass validation workflow.strictHandle([startEvent], (sendEvent, start) => { sendEvent(processEvent.with(123)); // ✅ This is allowed }); // This would fail at compile time and runtime workflow.strictHandle([startEvent], (sendEvent, start) => { // sendEvent(disallowedEvent.with("disallowed")); // ❌ This would cause an error // sendEvent(resultEvent.with("result")); // ❌ This would also cause an error }); ``` ## Error Handling LlamaIndex workflows provide built-in mechanisms for handling errors: ```ts import { createWorkflow, workflowEvent, getContext, } from "@llamaindex/workflow-core"; const startEvent = workflowEvent(); const processEvent = workflowEvent(); const errorEvent = workflowEvent(); const resultEvent = workflowEvent(); const workflow = createWorkflow(); workflow.handle([startEvent], (start) => { try { const num = Number.parseInt(start.data, 10); if (isNaN(num)) { throw new Error("Invalid number"); } return processEvent.with(num); } catch (err) { return errorEvent.with(err instanceof Error ? err : new Error(String(err))); } }); workflow.handle([processEvent], (event) => { return resultEvent.with(`Result: ${event.data * 2}`); }); workflow.handle([errorEvent], (event) => { return resultEvent.with(`Error: ${event.data.message}`); }); ``` You can also use the signal in `getContext()` to handle errors: ```ts workflow.handle([processEvent], () => { const { signal } = getContext(); signal.onabort = () => { console.error("Process aborted:", signal.reason); // Clean up resources }; // Your processing logic here }); ``` ## Connecting with Server Endpoints Workflow can be used as middleware in server frameworks like Express, Hono, or Fastify: ```ts import { Hono } from "hono"; import { serve } from "@hono/node-server"; import { createHonoHandler } from "@llamaindex/workflow-core/interrupter/hono"; import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core"; // Define events const queryEvent = workflowEvent(); const responseEvent = workflowEvent(); // Create workflow const workflow = createWorkflow(); workflow.handle([queryEvent], (event) => { const response = `Processed: ${event.data}`; return responseEvent.with(response); }); // Create Hono app const app = new Hono(); // Set up workflow endpoint app.post( "/workflow", createHonoHandler( workflow, async (ctx) => queryEvent.with(await ctx.req.text()), responseEvent, ), ); // Start server serve(app, ({ port }) => { console.log(`Server started at http://localhost:${port}`); }); ``` ## Next Steps Now that you've learned about basic workflow patterns, explore more advanced topics: - [Streaming with Workflows](./streaming.mdx) - [Advanced Event Handling](./advanced-events.mdx)