Execute Function API
The heart of every workflow step. Learn how to use the execute function to process data, access workflow state, and control flow.
Quick Start
Every workflow step has an execute
function that receives a context object:
.andThen({
id: "my-step",
execute: async ({ data, state, getStepData, suspend, resumeData }) => {
// Your logic here
return { result: "processed" };
}
})
What's in the Context?
The execute function receives one parameter - a context object with these properties:
1. data
- The Step's Input
This is the data flowing into your step:
- First step: Gets the workflow's initial input
- Other steps: Gets the output from the previous step
const workflow = createWorkflowChain({
input: z.object({ name: z.string() }),
// ...
})
.andThen({
id: "step-1",
execute: async ({ data }) => {
console.log(data.name); // Original input
return { ...data, step1: "done" };
},
})
.andThen({
id: "step-2",
execute: async ({ data }) => {
console.log(data.name); // Still there!
console.log(data.step1); // "done" - from previous step
return { ...data, step2: "also done" };
},
});
2. state
- Workflow Information
Contains metadata about the current workflow execution:
.andThen({
id: "log-info",
execute: async ({ data, state }) => {
console.log(state.executionId); // Unique ID for this run
console.log(state.userId); // Who's running it
console.log(state.conversationId); // Conversation context
console.log(state.input); // Original workflow input
console.log(state.startAt); // When it started
// userContext is a Map for custom data
const userRole = state.userContext?.get("role");
return data;
}
})
3. getStepData
- Access Any Previous Step
Get data from any step that has already executed:
.andThen({
id: "combine-results",
execute: async ({ data, getStepData }) => {
// Get data from a specific step
const step1Data = getStepData("step-1");
if (step1Data) {
console.log(step1Data.input); // What went INTO step-1
console.log(step1Data.output); // What came OUT of step-1
}
return data;
}
})
4. suspend
- Pause the Workflow
Pause execution and wait for external input (like human approval):
.andThen({
id: "wait-for-approval",
execute: async ({ data, suspend }) => {
if (data.amount > 1000) {
// This stops execution immediately
await suspend("Manager approval required");
// Code below never runs during suspension
}
return { ...data, approved: true };
}
})
5. resumeData
- Data from Resume
When a suspended workflow resumes, this contains the resume data:
.andThen({
id: "approval-step",
execute: async ({ data, suspend, resumeData }) => {
// Check if we're resuming
if (resumeData) {
// We're resuming! Use the approval decision
return {
...data,
approved: resumeData.approved,
approvedBy: resumeData.managerId
};
}
// First time through - suspend for approval
if (data.amount > 1000) {
await suspend("Needs approval");
}
// Auto-approve small amounts
return { ...data, approved: true, approvedBy: "auto" };
}
})
Complete Example
Here's a real-world example using all context properties:
import { createWorkflowChain } from "@voltagent/core";
import { z } from "zod";
const orderWorkflow = createWorkflowChain({
id: "order-processor",
name: "Order Processing",
input: z.object({
orderId: z.string(),
amount: z.number(),
items: z.array(z.string()),
}),
result: z.object({
status: z.string(),
trackingNumber: z.string(),
}),
})
.andThen({
id: "validate-order",
execute: async ({ data, state }) => {
console.log(`Processing order ${data.orderId} for user ${state.userId}`);
const isValid = data.items.length > 0 && data.amount > 0;
return { ...data, isValid };
},
})
.andThen({
id: "check-inventory",
execute: async ({ data, getStepData }) => {
// Only check if validation passed
const validation = getStepData("validate-order");
if (!validation?.output?.isValid) {
return { ...data, inStock: false };
}
// Check inventory for each item
const inStock = await checkInventory(data.items);
return { ...data, inStock };
},
})
.andThen({
id: "approve-payment",
execute: async ({ data, suspend, resumeData }) => {
// Handle resume from suspension
if (resumeData) {
return {
...data,
paymentApproved: resumeData.approved,
approvedBy: resumeData.approver,
};
}
// Auto-approve small amounts
if (data.amount <= 100) {
return { ...data, paymentApproved: true, approvedBy: "auto" };
}
// Suspend for manual approval
await suspend(`Payment approval needed for $${data.amount}`);
},
})
.andThen({
id: "ship-order",
execute: async ({ data, state }) => {
if (!data.paymentApproved) {
return {
status: "cancelled",
trackingNumber: "N/A",
};
}
// Ship the order
const tracking = await createShipment(data.orderId);
// Log completion
console.log(`Order ${data.orderId} shipped after ${Date.now() - state.startAt.getTime()}ms`);
return {
status: "shipped",
trackingNumber: tracking,
};
},
});
Different Step Types
Basic Steps (andThen
)
Transform data or perform operations:
.andThen({
id: "calculate-total",
execute: async ({ data }) => {
const total = data.items.reduce((sum, item) => sum + item.price, 0);
return { ...data, total };
}
})
AI Agent Steps (andAgent
)
The task function also gets the context:
.andAgent(
async ({ data }) => `Summarize this order: ${JSON.stringify(data.items)}`,
myAgent,
{ schema: z.object({ summary: z.string() }) }
)
Conditional Steps (andWhen
)
Only run when a condition is met:
.andWhen({
id: "apply-discount",
condition: async ({ data }) => data.total > 50,
step: andThen({
id: "discount",
execute: async ({ data }) => ({
...data,
total: data.total * 0.9,
discountApplied: true
})
})
})
Side Effects (andTap
)
Run code without changing the data:
.andTap({
id: "send-notification",
execute: async ({ data, state }) => {
await sendEmail(state.userId, `Order ${data.orderId} processed`);
// Return value ignored - data passes through
}
})
Suspend & Resume Deep Dive
For human-in-the-loop workflows, suspension is key:
.andThen({
id: "review-step",
execute: async ({ data, suspend, resumeData }) => {
// Step 1: Check if we're resuming
if (resumeData) {
console.log("Resuming with:", resumeData);
return { ...data, reviewed: true, reviewer: resumeData.userId };
}
// Step 2: Check if we need to suspend
if (data.requiresReview) {
// This immediately stops execution
await suspend("Document needs review", {
documentId: data.id,
reason: "High risk score"
});
// Never reaches here during suspension
}
// Step 3: Continue if no suspension needed
return { ...data, reviewed: true, reviewer: "auto" };
}
})
Important: When resumed, the step runs again from the beginning with resumeData
available.
For more details on suspension patterns, see Suspend & Resume.
Best Practices
1. Always Return New Objects
// ✅ Good - creates new object
return { ...data, processed: true };
// ❌ Bad - mutates existing object
data.processed = true;
return data;
2. Check Step Data Exists
const previousStep = getStepData("step-id");
if (previousStep) {
// Safe to use previousStep.output
}
3. Use Clear Step IDs
// ✅ Good - descriptive
id: "validate-payment";
// ❌ Bad - unclear
id: "step2";
4. Handle Errors Gracefully
execute: async ({ data }) => {
try {
const result = await riskyOperation(data);
return { ...data, result };
} catch (error) {
return { ...data, error: error.message, success: false };
}
};
5. Log Important Events
execute: async ({ data, state }) => {
console.log(`[${state.executionId}] Processing ${data.id}`);
const result = await process(data);
console.log(`[${state.executionId}] Completed with status: ${result.status}`);
return result;
};
TypeScript Types
The execute function is fully type-safe:
interface ExecuteContext<TData, TSuspendData = any, TResumeData = any> {
data: TData;
state: WorkflowState;
getStepData: (stepId: string) => { input: any; output: any } | undefined;
suspend: (reason?: string, data?: TSuspendData) => Promise<never>;
resumeData?: TResumeData;
}
Types flow automatically through your workflow - TypeScript knows what data is available at each step!