Agent
Orchestrate multi-step AI workflows with human-in-the-loop interactions.
What is an Agent?
An Agent is a multi-step workflow orchestrator in Tool Forge. While a Tool executes a single operation, an Agent coordinates multiple steps in a defined sequence—with the ability to branch, loop, and incorporate human feedback at any point.
Tool Forge makes it uniquely easy to build AI workflows with human-in-the-loop because of its native IO system. Each step in your agent can pause, collect user input, display results, and continue—all without leaving the workflow.
Agent vs Tool
| Aspect | Tool | Agent |
|---|---|---|
| Structure | Single handler function | Multiple steps with workflow |
| State | No built-in state | Context (shared state across steps) |
| Flow | Linear execution | Sequential, parallel, and conditional flows |
| Use Case | Simple operations | Complex multi-step workflows |
Agent Structure
Every agent is defined using the defineAgent() function:
import { defineAgent } from '@toolforge-js/sdk/components'
import * as z from 'zod'
export default defineAgent({
name: 'My Agent',
description: 'Description of what this agent does',
contextSchema: z.object({
// Define your agent's state structure
}),
steps: {
// Define individual steps
},
workflow: (builder) => {
// Define how steps connect
},
bootstrap: async ({ io }) => {
// Optional: Initialize context before workflow starts
},
})Configuration Options
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name in the dashboard |
description | string | No | Brief description of the agent's purpose |
contextSchema | z.ZodType | Yes | Zod schema defining the context structure |
steps | Record<string, AgentStep> | Yes | Object containing all step definitions |
workflow | function | Yes | Function that defines step connections |
bootstrap | function | No | Initialization function to set up initial context |
Context: The Agent's State
The context is the shared state that flows through your agent. Think of it as the agent's memory—each step can read from it and update it, allowing information to flow between steps.
Defining Context
Use Zod to define a strongly-typed context schema:
contextSchema: z.object({
// Required fields
topic: z.string().describe('The topic to write about'),
// Optional fields with defaults
wordCount: z.number().default(500),
// Fields populated by steps
generatedContent: z.string().optional(),
userFeedback: z.string().optional(),
// Control flow fields
isApproved: z.boolean().default(false),
iterationCount: z.number().default(0),
}),The context schema is validated at runtime. If a step tries to set invalid data, the agent will throw an error with a clear message.
Reading Context
Each step receives the current context:
handler: async ({ context }) => {
// Access context values
const topic = context.topic
const wordCount = context.wordCount
// Use in your logic
const result = await generateContent(topic, wordCount)
return result
}Updating Context
Use the updateContext function to modify the context. It accepts either a partial object or an updater function:
handler: async ({ context, updateContext }) => {
// Option 1: Partial object (merges with existing context)
updateContext({
generatedContent: 'New content here',
isApproved: false,
})
// Option 2: Updater function (access previous values)
updateContext((prev) => ({
iterationCount: prev.iterationCount + 1,
lastUpdated: new Date().toISOString(),
}))
}Context updates are validated against your schema. Always ensure your updates match the expected types.
Steps
Steps are the individual units of work in an agent. Each step has a handler function that executes when the step is reached in the workflow.
Defining Steps
steps: {
ideaGeneration: {
name: 'Idea Generation', // Optional display name
description: 'Generates ideas', // Optional description
handler: async ({ context, updateContext, io, block, signal, metadata }) => {
// Step logic here
},
},
writing: {
name: 'Writing',
handler: async ({ context, updateContext }) => {
// Another step
},
},
},Step Handler Arguments
Each step handler receives the same arguments as a Tool handler, plus context management:
| Argument | Description |
|---|---|
context | Current state of the agent (read-only snapshot) |
updateContext | Function to update the agent's context |
io | IO methods for user interaction |
block | Block outputs for rich displays |
signal | AbortSignal for handling cancellation |
metadata | Step and agent metadata (sessionId, stepName, etc.) |
Human-in-the-Loop Steps
This is where Tool Forge shines. Steps can pause and wait for user input using IO methods:
feedbackIncorporation: {
name: 'Get Feedback',
handler: async ({ io, updateContext }) => {
// Show content and ask for approval
const isApproved = await io.confirm({
title: 'Are you satisfied with the content?',
okButtonLabel: 'Yes, continue',
cancelButtonLabel: 'No, provide feedback',
})
if (isApproved) {
updateContext({ isApproved: true })
return 'User approved the content'
}
// Collect detailed feedback
const feedback = await io.textInput({
label: 'What would you like to change?',
multiline: true,
})
updateContext({
userFeedback: feedback,
isApproved: false,
})
return `Feedback received: ${feedback}`
},
},Workflow Builder
The workflow builder defines how steps connect and execute. It provides two methods for creating flows:
flow(from, to)— Direct transition between stepsbranch(from, condition, targetMap)— Conditional routing based on context values
Linear Flow: flow()
Use flow() to create direct transitions between steps:
workflow: (builder) => {
return builder
.flow('START', 'step1') // START → step1
.flow('step1', 'step2') // step1 → step2
.flow('step2', 'END') // step2 → END
}Special Nodes
Every workflow has two special nodes:
START- The entry point (must have outgoing edges, no incoming edges)END- The exit point (must have incoming edges, no outgoing edges)
Conditional Branching: branch()
Use branch() to route to different steps based on context values.
Parameters:
| Parameter | Type | Description |
|---|---|---|
from | string | The step (or START) from which to branch |
condition | (context) => string | A function that receives the current context and returns a key from the targetMap |
targetMap | Record<string, string> | An object mapping condition return values to step names (or END) |
Why targetMap?
You might wonder why the condition function returns a key that maps to a step,
rather than returning the step name directly. This design enables
compile-time graph validation. By declaring all possible targets upfront
in targetMap, Tool Forge can validate your workflow structure before
execution—ensuring all referenced steps exist, detecting unreachable nodes,
and verifying that END is reachable from every branch path.
workflow: (builder) => {
return builder
.flow('START', 'generateContent')
.flow('generateContent', 'reviewContent')
.branch(
'reviewContent',
// Condition function - returns a key from targetMap
(context) => (context.isApproved ? 'APPROVED' : 'NEEDS_REVISION'),
// Target map - maps keys to step names
{
APPROVED: 'publishContent',
NEEDS_REVISION: 'generateContent', // Loop back!
},
)
.flow('publishContent', 'END')
}Creating Loops
Branches can point back to earlier steps, creating loops for iterative refinement:
.branch(
'feedbackStep',
(context) => {
if (context.isUserSatisfied) return 'DONE'
if (context.iterationCount > context.maxIterations) return 'DONE'
return 'RETRY'
},
{
DONE: 'finalStep',
RETRY: 'generationStep', // Loop back for another iteration
}
)Always include an exit condition in loops to prevent infinite execution. The agent has a built-in maximum iteration limit (1000 by default).
Parallel Flows
You can create parallel execution paths by adding multiple flows from the same step:
workflow: (builder) => {
return builder
.flow('START', 'fetchUserData')
.flow('START', 'fetchProductData') // Runs in parallel with above
.flow('fetchUserData', 'processData')
.flow('fetchProductData', 'processData')
.flow('processData', 'END')
}Bootstrap Function
The bootstrap function runs before the workflow starts. Use it to:
- Collect initial inputs from the user
- Set up the initial context
- Perform any pre-workflow setup
bootstrap: async ({ io, block }) => {
const topic = await io.textInput({
label: 'What topic would you like to write about?',
})
const wordCount = await io.numberInput({
label: 'Target word count',
defaultValue: 500,
validationSchema: z.number().min(100).max(5000),
})
// Return partial context - merged with schema defaults
return { topic, wordCount }
},The returned object is merged with the context schema's defaults to create the initial context.
Complete Example: Writer Agent
Here's a complete agent that demonstrates all concepts—context, steps, branching, loops, and human-in-the-loop:
import { defineAgent } from '@toolforge-js/sdk/components'
import { generateText } from 'ai'
import * as z from 'zod'
export default defineAgent({
name: 'Writer Agent',
description: 'An AI writing assistant with human feedback loops',
// Define the agent's state
contextSchema: z.object({
topic: z.string(),
wordCount: z.number().default(500),
selectedIdea: z.string().optional(),
content: z.string().optional(),
isUserSatisfied: z.boolean().default(false),
userFeedback: z.string().optional(),
iterationCount: z.number().default(0),
maxIterations: z.number().default(3),
}),
steps: {
ideaGeneration: {
name: 'Idea Generation',
description: 'Generate and select a writing idea',
handler: async ({ context, updateContext, io }) => {
// Generate ideas using AI
const ideas = await generateIdeas(context.topic)
// Human-in-the-loop: Let user select
const selected = await io.selectInput({
label: 'Select an idea to write about',
options: ideas.map((idea, i) => ({
label: idea,
value: i.toString(),
})),
})
updateContext({ selectedIdea: ideas[parseInt(selected)] })
return `Selected: ${ideas[parseInt(selected)]}`
},
},
writing: {
name: 'Writing',
handler: async ({ context, updateContext }) => {
const content = await generateText({
// ... AI configuration
prompt: context.userFeedback
? `Revise based on: ${context.userFeedback}`
: `Write about: ${context.selectedIdea}`,
})
updateContext({ content: content.text })
return content.text
},
},
feedbackIncorporation: {
name: 'Get Feedback',
handler: async ({ io, updateContext }) => {
const isOK = await io.confirm({
title: 'Are you satisfied with the content?',
})
if (isOK) {
updateContext({ isUserSatisfied: true })
return 'User approved!'
}
const feedback = await io.textInput({
label: 'What would you like to change?',
})
updateContext((prev) => ({
userFeedback: feedback,
isUserSatisfied: false,
iterationCount: prev.iterationCount + 1,
}))
return `Feedback: ${feedback}`
},
},
generateImage: {
name: 'Generate Image',
handler: async ({ context, block, io }) => {
// Generate and display image
const image = await generateImage(context.content)
await io.message({
title: 'Generated Image',
message: block.image({ url: image.url }),
})
},
},
},
// Define the workflow
workflow: (builder) => {
return builder
.flow('START', 'ideaGeneration')
.flow('ideaGeneration', 'writing')
.flow('writing', 'feedbackIncorporation')
.branch(
'feedbackIncorporation',
(context) =>
context.isUserSatisfied ||
context.iterationCount >= context.maxIterations
? 'DONE'
: 'REVISE',
{ DONE: 'generateImage', REVISE: 'writing' },
)
.flow('generateImage', 'END')
},
// Initialize context before workflow starts
bootstrap: async ({ io }) => {
const topic = await io.textInput({
label: 'What topic would you like to write about?',
})
const wordCount = await io.numberInput({
label: 'Target word count',
defaultValue: 500,
})
return { topic, wordCount }
},
})Workflow Validation
The workflow builder automatically validates your workflow for common errors:
| Validation | Description |
|---|---|
| START exists | Workflow must begin with START |
| END exists | Workflow must terminate at END |
| START has outgoing edges | START must connect to at least one step |
| END has no outgoing edges | END cannot connect to other steps |
| No orphan nodes | All steps must be reachable from START |
| END is reachable | At least one path must lead to END |
| Branch minimum targets | Branches must have at least 2 targets |
| No mixed edge types | A step cannot have both flow and branch edges |
Agent Metadata
Each step receives metadata about the current execution:
handler: async ({ metadata }) => {
console.log(metadata.sessionId) // Unique session ID
console.log(metadata.runnerId) // Runner executing this agent
console.log(metadata.agentId) // Agent identifier
console.log(metadata.stepName) // Current step name
console.log(metadata.stepDescription) // Current step description
}Best Practices
1. Design Context for Flow Control
Include fields specifically for controlling workflow branching:
contextSchema: z.object({
// Data fields
inputData: z.string(),
result: z.string().optional(),
// Flow control fields
isComplete: z.boolean().default(false),
needsRetry: z.boolean().default(false),
errorMessage: z.string().optional(),
})2. Keep Steps Focused
Each step should do one thing well:
// ✅ Good: Focused steps
steps: {
validateInput: { /* ... */ },
processData: { /* ... */ },
formatOutput: { /* ... */ },
}
// ❌ Bad: Monolithic step
steps: {
doEverything: { /* validate + process + format */ },
}3. Handle Edge Cases in Branches
Always account for unexpected states:
.branch(
'checkStatus',
(context) => {
if (context.status === 'success') return 'SUCCESS'
if (context.status === 'retry') return 'RETRY'
return 'ERROR' // Default fallback
},
{ SUCCESS: 'complete', RETRY: 'process', ERROR: 'handleError' }
)4. Use Bootstrap for Required Inputs
Collect essential data before the workflow starts:
bootstrap: async ({ io }) => {
// These are required for the workflow to function
const requiredField = await io.textInput({
label: 'Required field',
validationSchema: z.string().min(1),
})
return { requiredField }
}5. Limit Loop Iterations
Prevent infinite loops with counter checks:
contextSchema: z.object({
iterationCount: z.number().default(0),
maxIterations: z.number().default(5),
}),
// In branch condition
(context) =>
context.iterationCount >= context.maxIterations ? 'EXIT' : 'CONTINUE'Next Steps
- Tools - Learn about single-operation tools
- IO Methods - Explore all available input/output methods
- Runner - Understand how agents execute
- Quick Start - Build your first project