Skip to main content

Introduction

Workflow is a primitive API that allows you to define a series of steps and chain by the event-driven mechanism. It is a powerful tool to orchestrate the execution of multiple tasks in a specific order.

In this guide, we will walk you through the basic concepts of the Workflow API and how to use it in your application.

Getting Started

import { StartEvent, StopEvent, Workflow } from "@llamaindex/core/workflow";
import { OpenAI } from "@llamaindex/openai";

const myWorkflow = new Workflow({
verbose: true,
});

const openai = new OpenAI();

myWorkflow.addStep(StartEvent, async (_, event) => {
const { input } = event.data;
const response = await openai.complete({
prompt: `Translate English to French: ${input}`,
});
return new StopEvent({
result: response.text,
});
});

const { data } = await myWorkflow.run("Hello, world!");

console.log("Result:", data.result);

Context

When you want to share state between steps, you can use the context object.

For example, you can abstract OpenAI to the context level and set it after you create the workflow.

import { StartEvent, StopEvent, Workflow } from "@llamaindex/core/workflow";
import { OpenAI } from "@llamaindex/openai";
import { createInterface } from "readline/promises";

const myWorkflow = new Workflow({
verbose: true,
}).with({
llm: new OpenAI({
apiKey: process.env.OPENAI_API_KEY!,
}),
});

myWorkflow.addStep(StartEvent, async (context, event) => {
const { llm } = context;
const { input } = event.data;
const response = await llm.complete({
prompt: `Translate English to French: ${input}`,
});
return new StopEvent({
result: response.text,
});
});

const rl = createInterface({
input: process.stdin,
output: process.stdout,
});

const input = await rl.question("Input word or phrase: ");

const { data } = await myWorkflow.run(input);

console.log("Result:", data.result);

rl.close();

Dynamic Context

You can also set the context dynamically by using the with after you run a workflow.

import type { LLM } from "@llamaindex/core/llms";
import { StartEvent, StopEvent, Workflow } from "@llamaindex/core/workflow";
import { Ollama } from "@llamaindex/ollama";
import { OpenAI } from "@llamaindex/openai";
import { createInterface } from "readline/promises";

const myWorkflow = new Workflow({
verbose: false,
}).with({
llm: null! as LLM,
});

myWorkflow.addStep(StartEvent, async (context, event) => {
const { llm } = context;
const { input } = event.data;
const response = await llm.complete({
prompt: `Translate English to French: ${input}`,
});
return new StopEvent({
result: response.text,
});
});

const rl = createInterface({
input: process.stdin,
output: process.stdout,
});

const input = await rl.question("Input word or phrase: ");

const emptyRunner = myWorkflow.run(input);

const openaiResult = emptyRunner.with({
llm: new OpenAI({
apiKey: process.env.OPENAI_API_KEY!,
}),
});

const ollamaResult = emptyRunner.with({
llm: new Ollama({
model: "llama3.1",
}),
});

{
const { data } = await openaiResult;
console.log("OpenAI Result:", data.result);
}

{
const { data } = await ollamaResult;
console.log("Ollama3.1 Result:", data.result);
}

rl.close();
Input word or phrase: Hello, world!
OpenAI Result: Bonjour, le monde !
Ollama3.1 Result: Bonjour, monde !

Complex Workflow

We can define a more complex workflow that involves multiple steps and reviews.

import {
StartEvent,
StopEvent,
Workflow,
WorkflowEvent,
} from "@llamaindex/core/workflow";
import { OpenAI, SimpleKVStore } from "llamaindex";

const kvContext = new SimpleKVStore();

const MAX_REVIEWS = 3;

// Using the o1-preview model (see https://platform.openai.com/docs/guides/reasoning?reasoning-prompt-examples=coding-planning)
const llm = new OpenAI({ model: "o1-preview", temperature: 1 });

// example specification from https://platform.openai.com/docs/guides/reasoning?reasoning-prompt-examples=coding-planning
const specification = `Python app that takes user questions and looks them up in a
database where they are mapped to answers. If there is a close match, it retrieves
the matched answer. If there isn't, it asks the user to provide an answer and
stores the question/answer pair in the database.`;

// Create custom event types
export class CodeEvent extends WorkflowEvent<{ code: string }> {}
export class ReviewEvent extends WorkflowEvent<{
review: string;
code: string;
}> {}

// Helper function to truncate long strings
const truncate = (str: string) => {
const MAX_LENGTH = 60;
if (str.length <= MAX_LENGTH) return str;
return str.slice(0, MAX_LENGTH) + "...";
};

// the architect is responsible for writing the structure and the initial code based on the specification
const architect = async (context: SimpleKVStore, ev: StartEvent) => {
// get the specification from the start event and save it to context
await context.put("specification", ev.data.input);
const spec = await context.get("specification");
// write a message to send an update to the user
console.log(`Writing app using this specification: ${truncate(spec)}`);
const prompt = `Build an app for this specification: <spec>${spec}</spec>. Make a plan for the directory structure you'll need, then return each file in full. Don't supply any reasoning, just code.`;
const code = await llm.complete({ prompt });
return new CodeEvent({ code: code.text });
};

// the coder is responsible for updating the code based on the review
const coder = async (context: SimpleKVStore, ev: ReviewEvent) => {
// get the specification from the context
const spec = await context.get("specification");
// get the latest review and code
const { review, code } = ev.data;
// write a message to send an update to the user
console.log(`Update code based on review: ${truncate(review)}`);
const prompt = `We need to improve code that should implement this specification: <spec>${spec}</spec>. Here is the current code: <code>${code}</code>. And here is a review of the code: <review>${review}</review>. Improve the code based on the review, keep the specification in mind, and return the full updated code. Don't supply any reasoning, just code.`;
const updatedCode = await llm.complete({ prompt });
return new CodeEvent({ code: updatedCode.text });
};

// the reviewer is responsible for reviewing the code and providing feedback
const reviewer = async (context: SimpleKVStore, ev: CodeEvent) => {
// get the specification from the context
const spec = await context.get("specification");
// get latest code from the event
const { code } = ev.data;
// update and check the number of reviews
const numberReviews = ((await context.get("numberReviews")) ?? 0) + 1;
await context.put("numberReviews", numberReviews);
if (numberReviews > MAX_REVIEWS) {
// the we've done this too many times - return the code
console.log(`Already reviewed ${numberReviews - 1} times, stopping!`);
return new StopEvent({ result: code });
}
// write a message to send an update to the user
console.log(`Review #${numberReviews}: ${truncate(code)}`);
const prompt = `Review this code: <code>${code}</code>. Check if the code quality and whether it correctly implements this specification: <spec>${spec}</spec>. If you're satisfied, just return 'Looks great', nothing else. If not, return a review with a list of changes you'd like to see.`;
const review = (await llm.complete({ prompt })).text;
if (review.includes("Looks great")) {
// the reviewer is satisfied with the code, let's return the review
console.log(`Reviewer says: ${review}`);
return new StopEvent({ result: code });
}

return new ReviewEvent({ review, code });
};

const codeAgent = new Workflow().with(kvContext);
codeAgent.addStep(StartEvent, architect, { outputs: CodeEvent });
codeAgent.addStep(ReviewEvent, coder, { outputs: CodeEvent });
codeAgent.addStep(CodeEvent, reviewer, { outputs: ReviewEvent });

const result = await codeAgent.run(specification);
console.log("Final code:\n", result.data.result);

Integrate with Next.js

Adding more loading indicators and error handling to the workflow, integrate with server action in Next.js.

"use server";
import {
StartEvent,
StopEvent,
Workflow,
WorkflowEvent,
} from "@llamaindex/core/workflow";
import { createStreamableUI } from "ai/rsc";
import { OpenAI, SimpleKVStore } from "llamaindex";
import { Loader2 } from "lucide-react";

const kvStore = new SimpleKVStore();

type Context = {
kVStore: SimpleKVStore;
ui: ReturnType<typeof createStreamableUI>;
};

const MAX_REVIEWS = 3;

// Using the o1-preview model (see https://platform.openai.com/docs/guides/reasoning?reasoning-prompt-examples=coding-planning)
const llm = new OpenAI({ model: "o1-preview", temperature: 1 });

class CodeEvent extends WorkflowEvent<{ code: string }> {}

class ReviewEvent extends WorkflowEvent<{
review: string;
code: string;
}> {}

const architect = async ({ kVStore, ui }: Context, ev: StartEvent) => {
await kVStore.put("specification", ev.data.input);
const spec = await kVStore.get("specification");
ui.update(
<div className="flex items-center text-sm text-gray-700">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<span>Generating initial code from the specification...</span>
</div>,
);
const prompt = `Build an app for this specification: <spec>${spec}</spec>. Make a plan for the directory structure you'll need, then return each file in full. Don't supply any reasoning, just code.`;
const code = await llm.complete({ prompt });
return new CodeEvent({ code: code.text });
};

const coder = async ({ kVStore, ui }: Context, ev: ReviewEvent) => {
const spec = await kVStore.get("specification");
const { review, code } = ev.data;
ui.update(
<div className="flex items-center text-sm text-gray-700">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<span>Updating code based on the provided review...</span>
</div>,
);
const prompt = `We need to improve code that should implement this specification: <spec>${spec}</spec>. Here is the current code: <code>${code}</code>. And here is a review of the code: <review>${review}</review>. Improve the code based on the review, keep the specification in mind, and return the full updated code. Don't supply any reasoning, just code.`;
const updatedCode = await llm.complete({ prompt });
return new CodeEvent({ code: updatedCode.text });
};

const reviewer = async ({ kVStore, ui }: Context, ev: CodeEvent) => {
const spec = await kVStore.get("specification");
const { code } = ev.data;
const numberReviews = ((await kVStore.get("numberReviews")) ?? 0) + 1;
await kVStore.put("numberReviews", numberReviews);
if (numberReviews > MAX_REVIEWS) {
ui.update(
<div className="flex items-center text-sm text-red-600">
<span>Review limit exceeded. Stopping further reviews.</span>
</div>,
);
return new StopEvent({ result: code });
}
ui.update(
<div className="flex items-center text-sm text-gray-700">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<span>Reviewing the code for compliance and quality...</span>
</div>,
);
const prompt = `Review this code: <code>${code}</code>. Check if the code quality and whether it correctly implements this specification: <spec>${spec}</spec>. If you're satisfied, just return 'Looks great', nothing else. If not, return a review with a list of changes you'd like to see.`;
const review = (await llm.complete({ prompt })).text;
if (review.includes("Looks great")) {
ui.update(
<div className="flex items-center text-sm text-green-600">
<span>Final review completed: {review}</span>
</div>,
);
return new StopEvent({ result: code });
}

return new ReviewEvent({ review, code });
};

const codeAgent = new Workflow<string, string, Context>().with({
kVStore: kvStore,
ui: null! as ReturnType<typeof createStreamableUI>,
});
codeAgent.addStep(StartEvent, architect, { outputs: CodeEvent });
codeAgent.addStep(ReviewEvent, coder, { outputs: CodeEvent });
codeAgent.addStep(CodeEvent, reviewer, { outputs: [ReviewEvent, StopEvent] });

export async function run(specification: string) {
"use server";
const ui = createStreamableUI();
const result = codeAgent.run(specification).with({
kVStore: kvStore,
ui,
});
return {
result: result.then(({ data }) => {
ui.done();
return data.result;
}),
ui: ui.value,
};
}
"use client";

import { run } from "@/actions/app-creator";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Code2, Loader2 } from "lucide-react";
import { type ReactNode, useState } from "react";

const defaultSpecification = `Python app that takes user questions and looks them up in a
database where they are mapped to answers. If there is a close match, it retrieves
the matched answer. If there isn't, it asks the user to provide an answer and
stores the question/answer pair in the database.`;

export default function AppCreator() {
const [specification, setSpecification] = useState("");
const [code, setCode] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [ui, setUi] = useState<ReactNode | null>(null);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!specification.trim()) {
setError("Please enter a description for your app.");
return;
}
setError("");
setIsLoading(true);

const { ui, result } = await run(specification);

setUi(ui);
result.then(setCode);

setIsLoading(false);
};

return (
<div className="max-w-2xl mx-auto p-6 space-y-6">
<h1 className="text-3xl font-bold text-center">AI App Creator</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label
htmlFor="app-description"
className="block text-sm font-medium text-gray-700 mb-1"
>
Describe your app
</label>
<Textarea
id="specification"
value={specification}
onChange={(e) => setSpecification(e.target.value)}
placeholder={defaultSpecification}
className="w-full h-32"
disabled={isLoading}
/>
</div>
{ui}
{error && <p className="text-red-500 text-sm">{error}</p>}
<Button type="submit" disabled={isLoading} className="w-full">
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating App...
</>
) : (
<>
<Code2 className="mr-2 h-4 w-4" />
Create App
</>
)}
</Button>
</form>
{code && (
<div className="mt-6">
<h2 className="text-xl font-semibold mb-2">Generated Python Code:</h2>
<Textarea
value={code}
readOnly
className="w-full h-64 font-mono text-sm bg-gray-100"
/>
</div>
)}
</div>
);
}