Overview
Note: Workflows are in Preview The Workflows API is currently in a preview state. This means it is stable and usable, but there may be breaking changes in future releases as we gather feedback and refine the developer experience.
Build powerful, type-safe AI workflows. Go from a simple chain of functions to a complex, multi-step process that combines your code, AI models, and conditional logic with ease.
What are Workflows?
A workflow is a chain of steps. Each step does something and passes its result to the next step.
workflow = step1 → step2 → step3 → result
That's it. The power comes from what each step can do: run code, call AI, make decisions, run in parallel.
Get Started in 2 Minutes
Let's build a workflow from scratch. We'll start with a simple function, add AI, and then introduce conditional logic. Each step will show the complete, runnable code.
1. Create a Basic Workflow
First, create a workflow that takes a name and returns a greeting. This is the simplest form of a workflow: a single step that processes some data.
import { VoltAgent, createWorkflowChain } from "@voltagent/core";
import { z } from "zod";
// Define the workflow's shape: its inputs and final output
const workflow = createWorkflowChain({
id: "greeter",
name: "Greeter Workflow",
// A detailed description for VoltOps or team clarity
purpose: "A simple workflow to generate a greeting for a given name.",
input: z.object({ name: z.string() }),
result: z.object({ greeting: z.string() }),
})
// Add the first step: a function to create the greeting
.andThen({
id: "create-greeting",
execute: async ({ data }) => {
return { greeting: `Hello, ${data.name}!` };
},
});
// Run it!
new VoltAgent({ workflows: { workflow } });
const result = await workflow.run({ name: "World" });
console.log(result.result);
// Output: { greeting: 'Hello, World!' }
2. Add AI Intelligence
Now, let's enhance our workflow. We'll add an AI agent to analyze the sentiment of the greeting message. Notice how we add new imports, define an agent, and chain the .andAgent()
step.
import { VoltAgent, createWorkflowChain, Agent } from "@voltagent/core";
import { z } from "zod";
import { openai } from "@ai-sdk/openai";
// Define an AI agent to use in our workflow
const agent = new Agent({
name: "Analyzer",
model: openai("gpt-4o-mini"),
instructions: "You are a text analyzer.",
});
const workflow = createWorkflowChain({
id: "greeter",
name: "Greeter Workflow",
input: z.object({ name: z.string() }),
// The final result now includes the sentiment
result: z.object({
greeting: z.string(),
sentiment: z.string(),
}),
})
.andThen({
id: "create-greeting",
execute: async ({ data }) => {
return { greeting: `Hello, ${data.name}!` };
},
})
// Add the new AI step to the chain
.andAgent(
async ({ data }) => `Analyze the sentiment of this greeting: "${data.greeting}"`,
agent,
{
schema: z.object({ sentiment: z.string().describe("e.g., positive, neutral, negative") }),
}
)
.andThen({
id: "combine-results",
execute: async ({ data, getStepData }) => {
const greeting = getStepData("create-greeting")?.output.greeting || "";
const sentiment = data.sentiment;
return { greeting, sentiment };
},
});
// Run the enhanced workflow
new VoltAgent({ workflows: { workflow } });
const result = await workflow.run({ name: "World" });
console.log(result.result);
// Output: { greeting: 'Hello, World!', sentiment: 'positive' }
3. Add Conditional Logic
Finally, let's add a step that only runs if a condition is met. We'll check if the input name is long and add a flag. The .andWhen()
step is perfect for this. We also update the final result
schema to include the new optional field.
import { VoltAgent, createWorkflowChain, Agent, andThen } from "@voltagent/core";
import { z } from "zod";
import { openai } from "@ai-sdk/openai";
const agent = new Agent({
name: "Analyzer",
model: openai("gpt-4o-mini"),
instructions: "You are a text analyzer.",
});
const workflow = createWorkflowChain({
id: "greeter",
name: "Greeter Workflow",
input: z.object({ name: z.string() }),
// The final result now includes an optional 'isLongName' field
result: z.object({
greeting: z.string(),
sentiment: z.string(),
isLongName: z.boolean().optional(),
}),
})
.andThen({
id: "create-greeting",
execute: async ({ data }) => {
return { greeting: `Hello, ${data.name}!` };
},
})
.andAgent(
async ({ data }) => `Analyze the sentiment of this greeting: "${data.greeting}"`,
agent,
{
schema: z.object({ sentiment: z.string().describe("e.g., positive, neutral, negative") }),
}
)
.andThen({
id: "combine-results",
execute: async ({ data, getStepData }) => {
const greeting = getStepData("create-greeting")?.output.greeting || "";
const sentiment = data.sentiment;
return { greeting, sentiment };
},
})
// Add a conditional step
.andWhen({
id: "check-name-length",
condition: async ({ data }) => data.greeting.length > 15,
step: andThen({
id: "set-long-name-flag",
execute: async ({ data }) => ({ ...data, isLongName: true }),
}),
});
// Run with a long name to trigger the conditional step
new VoltAgent({ workflows: { workflow } });
const longNameResult = await workflow.run({ name: "Alexanderson" });
console.log(longNameResult.result);
// Output: { greeting: 'Hello, Alexanderson!', sentiment: 'positive', isLongName: true }
// Run with a short name to skip the conditional step
const shortNameResult = await workflow.run({ name: "Alex" });
console.log(shortNameResult.result);
// Output: { greeting: 'Hello, Alex!', sentiment: 'positive' }
How It Works
Workflows are built on three core principles:
1. A Chain of Steps
You build a workflow by chaining steps together using methods like .andThen()
, .andAgent()
, and .andWhen()
. Each step performs a specific action, like running code, calling an AI, or making a decision.
createWorkflowChain(...)
.andThen(...) // Step 1: Run some code
.andAgent(...) // Step 2: Call an AI
.andWhen(...) // Step 3: Maybe run another step
2. Data Flows Through the Chain
The output of one step becomes the input for the next. The data object is automatically merged, so you can access results from all previous steps.
3. Automatic Type Safety
As data flows through the workflow, TypeScript types are automatically inferred and updated at each step. This means you get full autocompletion and type-checking, preventing common errors.
import { z } from "zod";
import { Agent, createWorkflowChain } from "@voltagent/core";
declare const agent: Agent<any>;
createWorkflowChain({
id: "type-safe-workflow",
name: "Type-Safe Workflow",
input: z.object({ email: z.string() }),
result: z.object({ success: z.boolean() }),
})
// `data` is typed as { email: string }
.andThen({
id: "add-user-id",
execute: async ({ data }) => {
// data.email is available and type-safe
return { ...data, userId: "user-123" };
},
})
// `data` is now typed as { email: string, userId: string }
.andAgent(
({ data }) => `Welcome ${data.userId}`, // data.userId is available!
agent,
{
schema: z.object({ welcomeMessage: z.string() }),
}
)
// `data` is now typed as { ..., welcomeMessage: string }
.andThen({
id: "finalize",
execute: async ({ data }) => {
// data.welcomeMessage is available!
return { success: true };
},
});
Builder vs. Runnable: toWorkflow()
When you use createWorkflowChain
, you are creating a builder object (WorkflowChain
). Each call to .andThen()
, .andAgent()
, etc., modifies this builder and returns it, allowing you to chain methods.
This builder is not the final, runnable workflow itself. It's the blueprint.
There are two ways to run your workflow:
1. The Shortcut: .run()
Calling .run()
directly on the chain is a convenient shortcut. Behind the scenes, it first converts your chain into a runnable workflow and then immediately executes it. This is great for most use cases.
// .run() builds and executes in one step
const result = await workflow.run({ name: "World" });
2. The Reusable Way: .toWorkflow()
The WorkflowChain
builder has a .toWorkflow()
method that converts your blueprint into a permanent, reusable Workflow
object. You can store this object, pass it to other functions, or run it multiple times without rebuilding the chain.
This is powerful for creating modular and testable code.
import { VoltAgent, createWorkflowChain } from "@voltagent/core";
import { z } from "zod";
// 1. Define the chain (the builder)
const greeterChain = createWorkflowChain({
id: "reusable-greeter",
name: "Reusable Greeter",
input: z.object({ name: z.string() }),
result: z.object({ greeting: z.string() }),
}).andThen({
id: "create-greeting",
execute: async ({ name }) => ({ greeting: `Hello, ${name}!` }),
});
// 2. Convert the builder into a runnable, reusable workflow object
const runnableGreeter = greeterChain.toWorkflow();
// 3. Now you can run it as many times as you want
new VoltAgent({ workflows: { runnableGreeter } });
const result1 = await runnableGreeter.run({ name: "Alice" });
console.log(result1.result); // { greeting: 'Hello, Alice!' }
const result2 = await runnableGreeter.run({ name: "Bob" });
console.log(result2.result); // { greeting: 'Hello, Bob!' }
This distinction allows you to define your workflow logic once and execute it in different contexts or at different times.