AndAgent
Add AI to your workflow. Get structured, typed responses from language models.
Quick Start
import { createWorkflowChain, Agent } from "@voltagent/core";
import { z } from "zod";
// Create an agent
const agent = new Agent({
name: "Assistant",
// Pass an ai-sdk model directly
model: "openai/gpt-4o-mini",
instructions: "Be concise and helpful",
});
// Use it in a workflow
const workflow = createWorkflowChain({
id: "analyze-text",
input: z.object({ text: z.string() }),
}).andAgent(async ({ data }) => `Analyze this text: ${data.text}`, agent, {
schema: z.object({
sentiment: z.enum(["positive", "negative", "neutral"]),
summary: z.string(),
}),
});
const result = await workflow.run({ text: "I love this!" });
// Result: { sentiment: "positive", summary: "Expression of enthusiasm" }
How It Works
andAgent = AI prompt + structured output schema (Zod or Output.*):
.andAgent(
prompt, // What to ask the AI
agent, // Which AI to use
{ schema }, // Zod schema or ai-sdk Output.* spec
map? // Optional: merge/shape output with existing data
)
If you pass a function for prompt, it must return a Promise (use async) and resolve to a string, UIMessage[], or ModelMessage[].
Important: andAgent uses generateText. If you pass a Zod schema, it is wrapped with Output.object. If you pass an Output.* spec, it is used directly. This means:
- ✅ You get structured, typed responses based on your schema
- ✅ The agent can use tools during this step
- ❌ Streaming is not supported (response returns when complete)
By default, the step result replaces the workflow data with the agent output. If you need to keep previous data, use the optional mapper (4th argument) to merge or reshape the output.
Need streaming or custom tool handling? Use andThen to call the agent directly with streamText or generateText.
Function Signature
// Simple prompt (string)
.andAgent("Summarize this", agent, { schema })
// Dynamic prompt from data (async)
.andAgent(async ({ data }) => `Analyze: ${data.text}`, agent, { schema })
// Advanced: pass ai-sdk v5 ModelMessage[] (multimodal)
.andAgent(
async ({ data }) => [
{ role: 'user', content: [{ type: 'text', text: `Hello ${data.name}` }] },
],
agent,
{ schema }
)
// Advanced: pass UIMessage[]
.andAgent(
async ({ data }) => [
{ id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: data.prompt }] },
],
agent,
{ schema }
)
// Merge agent output with existing data
.andAgent(
async ({ data }) => `Classify: ${data.email}`,
agent,
{ schema: z.object({ type: z.enum(["support", "sales", "spam"]) }) },
(output, { data }) => ({ ...data, emailType: output })
)
// Use Output.* for non-object outputs (arrays, choices, json, text)
// (Requires: import { Output } from "ai";)
.andAgent(
async ({ data }) => `List tags for: ${data.topic}`,
agent,
{ schema: Output.array({ element: z.string() }) }
)
Middleware and Retries
andAgent passes the config object to agent.generateText, so you can use
inputMiddlewares, outputMiddlewares, maxMiddlewareRetries, and maxRetries
per step.
import { Agent, createInputMiddleware, createOutputMiddleware } from "@voltagent/core";
import { createWorkflowChain } from "@voltagent/core";
import { z } from "zod";
const redactInput = createInputMiddleware({
name: "RedactPII",
handler: ({ input }) => {
if (typeof input !== "string") return input;
return input.replace(/\b\d{16}\b/g, "[redacted]");
},
});
const requireSource = createOutputMiddleware<string>({
name: "RequireSource",
handler: ({ output, abort }) => {
if (!output.includes("source:")) {
abort("Missing source in summary", { retry: true });
}
return output;
},
});
const agent = new Agent({
name: "Assistant",
model: "openai/gpt-4o-mini",
instructions: "Summarize documents with sources.",
});
createWorkflowChain({
id: "doc-summary",
input: z.object({ text: z.string() }),
}).andAgent(async ({ data }) => `Summarize and include source lines: ${data.text}`, agent, {
schema: z.object({
summary: z.string(),
sources: z.array(z.string()),
}),
inputMiddlewares: [redactInput],
outputMiddlewares: [requireSource],
maxMiddlewareRetries: 1,
maxRetries: 2,
});
Retry behavior:
maxRetriescontrols LLM retries for the selected model (total attempts =maxRetries + 1).maxMiddlewareRetriescontrols retries triggered byabort(..., { retry: true }).- A middleware retry restarts the step: middlewares, guardrails, hooks, model selection, and fallback.
- Output middleware runs on the model text and can trigger retries. It does not change the parsed
object returned by
andAgent.
Common Patterns
Text Analysis
.andAgent(
async ({ data }) => `Analyze sentiment of: ${data.review}`,
agent,
{
schema: z.object({
sentiment: z.enum(["positive", "negative", "neutral"]),
score: z.number().min(0).max(1),
keywords: z.array(z.string())
})
}
)
Content Generation
.andAgent(
async ({ data }) => `Write a ${data.tone} email about ${data.topic}`,
agent,
{
schema: z.object({
subject: z.string(),
body: z.string(),
suggestedSendTime: z.string()
})
}
)
Data Extraction
.andAgent(
async ({ data }) => `Extract key information from: ${data.document}`,
agent,
{
schema: z.object({
people: z.array(z.string()),
dates: z.array(z.string()),
locations: z.array(z.string()),
mainTopic: z.string()
})
}
)
Arrays and Choices
Requires import { Output } from "ai";
.andAgent(
async ({ data }) => `Give 3 tags for: ${data.topic}`,
agent,
{ schema: Output.array({ element: z.string() }) }
)
.andAgent(
async ({ data }) => `Pick a category for: ${data.title}`,
agent,
{ schema: Output.choice({ options: ["news", "blog", "doc"] }) }
)
Merge Output With Existing Data
.andAgent(
async ({ data }) => `What type of email is this: ${data.email}`,
agent,
{
schema: z.object({
type: z.enum(["support", "sales", "spam"]),
priority: z.enum(["low", "medium", "high"]),
}),
},
(output, { data }) => ({ ...data, emailType: output })
)
Dynamic Prompts
Build prompts from workflow data:
.andAgent(
async ({ data }) => {
// Adjust prompt based on data
if (data.userLevel === "beginner") {
return `Explain in simple terms: ${data.question}`;
}
return `Provide technical details about: ${data.question}`;
},
agent,
{ schema: z.object({ answer: z.string() }) }
)
Chaining with Other Steps
Combine AI with logic:
createWorkflowChain({ id: "smart-email" })
// Step 1: Classify with AI
.andAgent(
async ({ data }) => `What type of email is this: ${data.email}`,
agent,
{
schema: z.object({
type: z.enum(["support", "sales", "spam"]),
priority: z.enum(["low", "medium", "high"]),
}),
},
(output, { data }) => ({ ...data, emailType: output })
)
// Step 2: Route based on classification
.andThen({
id: "route-email",
execute: async ({ data }) => {
if (data.emailType.type === "spam") {
return { action: "delete" };
}
return {
action: "forward",
to: data.emailType.type === "support" ? "support@" : "sales@",
};
},
});
Streaming or Custom Tool Handling
andAgent supports tools, but it only returns the structured output when the step completes. Use andThen when you need streaming tokens or to inspect tool calls/results directly:
import { Agent, createTool } from "@voltagent/core";
import { z } from "zod";
const getWeatherTool = createTool({
name: "get_weather",
description: "Get weather for a location",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => {
return { temp: 72, condition: "sunny" };
},
});
const agent = new Agent({
name: "Assistant",
model: "openai/gpt-4o-mini",
tools: [getWeatherTool],
});
// Use andThen to call the agent directly when you need streaming or tool call inspection
createWorkflowChain({ id: "weather-flow" }).andThen({
id: "get-weather",
execute: async ({ data }) => {
// Call streamText/generateText directly for streaming or tool call handling
const result = await agent.generateText(`What's the weather in ${data.city}?`);
return { response: result.text };
},
});
Best Practices
- Keep prompts clear - AI performs better with specific instructions
- Use enums for categories -
z.enum()ensures valid options - Add descriptions to schema fields - Helps AI understand what you want
- Handle edge cases - Check for missing or low-confidence results
- Need streaming or tool inspection? - Use
andThenwith direct agent calls instead ofandAgent