What’s an Agent?
An agent is a reusable unit of work with:
Inputs : Parameters it accepts
Operation : What it does (code, think, http, storage, etc.)
Outputs : Data it returns
Agents are automatically discovered from the agents/ directory at build time (v1.12+) and can be used across multiple ensembles.
Explore the Template Agent
Your project already includes a working agent! Let’s explore agents/examples/hello/:
agents/examples/hello/
├── agent.yaml # Agent configuration
└── index.ts # Agent implementation
agent.yaml
name : hello
operation : code
description : Simple greeting function
schema :
input :
name : string
style : string?
output :
message : string
This declares:
Operation type : code (runs JavaScript/TypeScript)
Input schema : Accepts name (required) and style (optional)
Output schema : Returns a message string
index.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor' ;
export default function hello ({ input } : AgentExecutionContext ) {
const { name , style } = input as { name : string ; style ?: string };
const styles = {
formal: `Good day, ${ name } . It is a pleasure to make your acquaintance.` ,
casual: `Hey ${ name } ! What's up?` ,
enthusiastic: `OMG ${ name } ! SO EXCITED TO MEET YOU!!!`
};
const message = style && style in styles
? styles [ style as keyof typeof styles ]
: `Hello, ${ name } ! Welcome to Conductor.` ;
return { message };
}
Note the signature : Agents use AgentExecutionContext which provides:
input - Your agent’s parameters
env - Cloudflare bindings (KV, D1, AI, etc.)
ctx - ExecutionContext (waitUntil, etc.)
This signature works everywhere: direct calls, ensembles, and tests.
Understanding Operation Types
Agents use different operations based on what they need to do:
operation: code
When to use : Run custom TypeScript/JavaScript logic
Requires : Function implementation in index.ts
API keys needed : ❌ No
name : greeter
operation : code
description : Custom greeting logic
schema :
input :
name : string
output :
greeting : string
operation: think
When to use : Call LLM models for reasoning, analysis, generation
Requires : Provider and model configuration
API keys needed : ✅ Yes (OpenAI, Anthropic) or Cloudflare Workers AI
name : analyzer
operation : think
description : Analyze text with AI
config :
provider : anthropic
model : claude-3-5-sonnet-20241022
prompt : |
Analyze this text: ${input.text}
Provide key insights.
schema :
input :
text : string
output :
analysis : string
operation: http
When to use : Make HTTP requests to external APIs
Requires : URL and method configuration
API keys needed : Depends on API
name : fetcher
operation : http
description : Fetch data from API
config :
url : https://api.example.com/data
method : GET
headers :
Authorization : Bearer ${env.API_KEY}
schema :
output :
data : object
operation: data
When to use : Query databases (KV, D1, R2)
Requires : Database binding in wrangler.toml
API keys needed : ❌ No (uses Cloudflare bindings)
name : db-query
operation : data
description : Query D1 database
config :
backend : d1
binding : DB
operation : query
sql : SELECT * FROM users WHERE id = ?
params :
- ${input.user_id}
schema :
input :
user_id : string
output :
user : object
See all operations
Critical: Agent Signatures for Ensembles
All agents MUST use the AgentExecutionContext signature to work in ensembles!
The Correct Pattern ✅
import type { AgentExecutionContext } from '@ensemble-edge/conductor' ;
interface MyAgentInput {
param1 : string ;
param2 : number ;
}
interface MyAgentOutput {
result : string ;
}
export default function myAgent ({ input , env , ctx } : AgentExecutionContext ) : MyAgentOutput {
// Destructure your parameters from input
const { param1 , param2 } = input as MyAgentInput ;
// Your logic here
const result = `Processed: ${ param1 } with ${ param2 } ` ;
return { result };
}
Why This Signature?
When called through an ensemble, Conductor wraps your parameters:
// Ensemble passes:
{
input : { param1 : 'hello' , param2 : 42 }, // Your parameters
env : { /* Cloudflare bindings */ }, // KV, D1, AI, etc.
ctx : { /* ExecutionContext */ } // waitUntil, etc.
}
Benefits :
✅ Works in ensembles (orchestrated workflows)
✅ Works with direct calls
✅ Works in tests
✅ Access to Cloudflare bindings (env)
✅ Access to ExecutionContext (ctx)
Wrong Pattern (Don’t Do This) ❌
// ❌ DOESN'T WORK IN ENSEMBLES
export default function myAgent ({ param1 , param2 } : MyInput ) {
return { result: param1 + param2 };
}
This only works for direct function calls, but fails in ensembles because the parameters are wrapped in input.
Using env and ctx
The signature gives you access to powerful features:
export default async function myAgent ({ input , env , ctx } : AgentExecutionContext ) {
const { query } = input as { query : string };
// Access KV storage
const cached = await env . KV . get ( query );
if ( cached ) return JSON . parse ( cached );
// Use AI binding
const result = await env . AI . run ( '@cf/meta/llama-3.1-8b-instruct' , {
prompt: query
});
// Schedule background work
ctx . waitUntil ( env . KV . put ( query , JSON . stringify ( result )));
return { result };
}
Quick Rule : Always use AgentExecutionContext signature. It’s the only pattern that works everywhere!
Test the Hello Agent
The template includes working tests. Let’s look at tests/basic.test.ts:
import { describe , it , expect } from 'vitest' ;
import { Executor , MemberLoader } from '@ensemble-edge/conductor' ;
import { stringify } from 'yaml' ;
import helloWorldYAML from '../ensembles/hello-world.yaml' ;
import greetConfig from '../agents/examples/hello/agent.yaml' ;
import greetFunction from '../agents/hello' ;
describe ( 'Hello Agent Test' , () => {
it ( 'should execute successfully' , async () => {
// Setup with proper ExecutionContext mock
const env = {} as Env ;
const ctx = {
waitUntil : ( promise : Promise < any >) => promise ,
passThroughOnException : () => {}
} as ExecutionContext ;
const executor = new Executor ({ env , ctx });
const loader = new MemberLoader ({ env , ctx });
// Register hello agent
const greetMember = loader . registerAgent ( greetConfig , greetFunction );
executor . registerAgent ( greetMember );
// Execute the ensemble
const result = await executor . executeFromYAML (
stringify ( helloWorldYAML ),
{ name: 'World' }
);
// Verify result
expect ( result . success ). toBe ( true );
expect ( result . value . output . greeting ). toContain ( 'Hello' );
});
});
Run it:
All tests should pass! ✅
Create Your First Custom Agent
Now that you understand how agents work, let’s create a new one.
Step 1: Create Agent Directory
mkdir -p agents/user/greeter
Step 2: Define the Agent
Create agents/user/greeter/agent.yaml:
name : greeter
operation : code
description : Generates personalized greetings with different styles
schema :
input :
name : string
time_of_day : string?
language : string?
output :
greeting : string
timestamp : number
Step 3: Implement the Agent
Create agents/user/greeter/index.ts:
import type { AgentExecutionContext } from '@ensemble-edge/conductor' ;
export default function greeter ({ input } : AgentExecutionContext ) {
const {
name ,
time_of_day = 'day' ,
language = 'en'
} = input as {
name : string ;
time_of_day ?: string ;
language ?: string ;
};
const greetings : Record < string , Record < string , string >> = {
en: {
morning: `Good morning, ${ name } !` ,
afternoon: `Good afternoon, ${ name } !` ,
evening: `Good evening, ${ name } !` ,
day: `Hello, ${ name } !`
},
es: {
morning: `Buenos días, ${ name } !` ,
afternoon: `Buenas tardes, ${ name } !` ,
evening: `Buenas noches, ${ name } !` ,
day: `Hola, ${ name } !`
},
fr: {
morning: `Bonjour, ${ name } !` ,
afternoon: `Bon après-midi, ${ name } !` ,
evening: `Bonsoir, ${ name } !` ,
day: `Bonjour, ${ name } !`
}
};
const langGreetings = greetings [ language ] || greetings . en ;
const greeting = langGreetings [ time_of_day ] || langGreetings . day ;
return {
greeting ,
timestamp: Date . now ()
};
}
Step 4: Rebuild
Agents are auto-discovered at build time:
Step 5: Use Your Agent
Create ensembles/greeting-workflow.yaml:
name : greeting-workflow
description : Generate personalized greetings
trigger :
- type : cli
command : greet
agents :
- name : greet
operation : code
config :
handler : greeter # Reference your new agent
flow :
- agent : greet
output :
greeting : ${greet.output.greeting}
timestamp : ${greet.output.timestamp}
Step 6: Test It
Create tests/greeter.test.ts:
import { describe , it , expect } from 'vitest' ;
import { Executor , MemberLoader } from '@ensemble-edge/conductor' ;
import { stringify } from 'yaml' ;
import greetingWorkflow from '../ensembles/greeting-workflow.yaml' ;
import greeterConfig from '../agents/user/greeter/agent.yaml' ;
import greeterFunction from '../agents/user/greeter' ;
describe ( 'Greeter Agent' , () => {
it ( 'should generate greetings in different languages' , async () => {
const env = {} as Env ;
const ctx = {
waitUntil : ( promise : Promise < any >) => promise ,
passThroughOnException : () => {}
} as ExecutionContext ;
const executor = new Executor ({ env , ctx });
const loader = new MemberLoader ({ env , ctx });
const greeterMember = loader . registerAgent ( greeterConfig , greeterFunction );
executor . registerAgent ( greeterMember );
const result = await executor . executeFromYAML (
stringify ( greetingWorkflow ),
{ name: 'Alice' , time_of_day: 'morning' , language: 'es' }
);
expect ( result . success ). toBe ( true );
expect ( result . value . output . greeting ). toBe ( 'Buenos días, Alice!' );
});
});
Run: pnpm test
Agent with AI (operation: think)
Let’s create an agent that uses AI for more complex logic.
Create AI Analyzer Agent
Create agents/user/analyzer/agent.yaml:
name : analyzer
operation : think
description : Analyzes text sentiment and extracts key themes
config :
provider : anthropic
model : claude-3-5-sonnet-20241022
prompt : |
Analyze the following text:
${input.text}
Provide a JSON response with:
- sentiment: positive, negative, or neutral
- confidence: 0-1 score
- themes: array of key themes
- summary: one sentence summary
Return only valid JSON.
schema :
input :
text : string
output :
sentiment : string
confidence : number
themes : array
summary : string
Note : This requires an Anthropic API key in your environment.
Use the Analyzer
Create ensembles/analyze-text.yaml:
name : analyze-text
description : Analyze text with AI
trigger :
- type : cli
command : analyze
agents :
- name : analyze
operation : think
config :
provider : anthropic
model : claude-3-5-sonnet-20241022
prompt : |
Analyze: ${input.text}
flow :
- agent : analyze
output :
analysis : ${analyze.output}
Auto-Discovery (v1.12+)
Zero-Config Agent Loading : Agents in the agents/ directory are automatically discovered at build time and registered with your application.
How It Works
Build-Time Discovery : Vite plugins scan agents/**/*.yaml during build
Virtual Modules : Agent configs and handlers are bundled into virtual:conductor-agents
Runtime Registration : MemberLoader.autoDiscover() loads all discovered agents automatically
Creating a New Agent
Just create the files - no imports or registration needed:
Create the directory: agents/user/my-agent/
Add agent.yaml (required)
Add index.ts (optional, for operation: code)
Rebuild: pnpm run build
Done! Your agent is now available at /api/v1/execute/agent/{name}
Using Auto-Discovered Agents
With the auto-discovery API (recommended):
// src/index.ts
import { createAutoDiscoveryAPI } from '@ensemble-edge/conductor/api'
import { agents } from 'virtual:conductor-agents'
import { ensembles } from 'virtual:conductor-ensembles'
export default createAutoDiscoveryAPI ({
agents , // All agents auto-discovered
ensembles , // All ensembles auto-discovered
autoDiscover: true ,
})
That’s it! No manual imports. No registration code. Just create YAML files.
Verification
List all discovered agents:
# After build
curl http://localhost:8787/api/v1/agents
# Returns:
{
"agents" : [
{ "name" : "hello", "operation": "code" },
{ "name" : "greeter", "operation": "code" },
{ "name" : "analyzer", "operation": "think" }
]
}
Testing with Manual Registration
Note : In unit tests, you can still use manual registration for clarity:
import { MemberLoader } from '@ensemble-edge/conductor'
import greetConfig from '../agents/user/greeter/agent.yaml'
import greetFunction from '../agents/user/greeter'
const loader = new MemberLoader ({ env , ctx })
loader . registerAgent ( greetConfig , greetFunction )
This is fine for tests! Manual registration is supported alongside auto-discovery.
Migration from v1.11
If you have existing manual registration code in your entry point:
Before (v1.11) :
import greetConfig from './agents/user/greet/agent.yaml'
import greetFunction from './agents/user/greet/index.ts'
// ... 50 more imports ...
const loader = new MemberLoader ({ env , ctx })
loader . registerAgent ( greetConfig , greetFunction )
// ... 50 more registrations ...
After (v1.12) :
import { createAutoDiscoveryAPI } from '@ensemble-edge/conductor/api'
import { agents } from 'virtual:conductor-agents'
import { ensembles } from 'virtual:conductor-ensembles'
export default createAutoDiscoveryAPI ({
agents ,
ensembles ,
autoDiscover: true ,
})
Saves 400+ lines of boilerplate!
See the Auto-Discovery guide for complete details.
Agent Patterns
Pattern 1: Simple Code Agent
Pure logic, no external dependencies:
// agents/user/calculator/index.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor' ;
export default function calculator ({ input } : AgentExecutionContext ) {
const { operation , a , b } = input as {
operation : string ;
a : number ;
b : number ;
};
const ops : Record < string , number > = {
add: a + b ,
subtract: a - b ,
multiply: a * b ,
divide: b !== 0 ? a / b : 0
};
return {
result: ops [ operation ] || 0
};
}
Pattern 2: HTTP Data Fetcher
Fetch from external APIs:
name : weather-fetcher
operation : http
description : Fetch weather data
config :
url : https://api.weather.com/current?city=${input.city}
method : GET
headers :
Authorization : Bearer ${env.WEATHER_API_KEY}
cache :
ttl : 1800 # Cache for 30 minutes
schema :
input :
city : string
output :
temperature : number
conditions : string
Pattern 3: Database Query
Query Cloudflare D1:
name : user-lookup
operation : data
description : Look up user by email
config :
backend : d1
binding : DB
operation : query
sql : |
SELECT id, name, email, created_at
FROM users
WHERE email = ?
LIMIT 1
params :
- ${input.email}
schema :
input :
email : string
output :
user : object?
Pattern 4: AI with Custom Logic
Combine AI with code:
name : smart-responder
operation : think
description : Generate contextual responses
config :
provider : anthropic
model : claude-3-5-sonnet-20241022
prompt : |
User message: ${input.message}
User history: ${input.history}
Generate a helpful response that:
1. Acknowledges their message
2. References their history
3. Provides actionable next steps
Keep it under 50 words.
schema :
input :
message : string
history : array
output :
response : string
Best Practices
1. Keep Agents Focused
Each agent should do ONE thing well:
✅ user-validator - Validates user data
❌ user-handler - Validates, stores, sends email, logs (too much!)
2. Use Descriptive Names
✅ email-sender, pdf-extractor, sentiment-analyzer
❌ helper, utils, processor
Always define schemas:
schema :
input :
user_id : string # UUID of the user
include_metadata : boolean # Whether to include extra fields
output :
user : object # User record with all fields
metadata : object? # Additional metadata if requested
4. Handle Errors Gracefully
import type { AgentExecutionContext } from '@ensemble-edge/conductor' ;
export default function myAgent ({ input } : AgentExecutionContext ) {
const { data } = input as { data : string };
try {
// Your logic
const result = processData ( data );
return { success: true , result };
} catch ( error ) {
return {
success: false ,
error: error instanceof Error ? error . message : 'Unknown error'
};
}
}
5. Use Caching for HTTP Agents
config :
url : https://api.example.com/data
cache :
ttl : 3600 # Cache for 1 hour
Reduces API calls and improves performance!
6. Test Your Agents
Always write tests for custom agents:
describe ( 'My Agent' , () => {
it ( 'should handle valid input' , async () => {
// Test success case
});
it ( 'should handle invalid input' , async () => {
// Test error case
});
it ( 'should respect timeout' , async () => {
// Test performance
});
});
Troubleshooting
Agent not found after creation
Problem : Created a new agent but it’s not availableFix : Rebuild to trigger auto-discovery:Agents are discovered at build time, not runtime.
ExecutionContext errors in tests
Problem : TypeError: this.ctx.waitUntil is not a functionFix : Use proper ExecutionContext mock:const ctx = {
waitUntil : ( promise : Promise < any >) => promise ,
passThroughOnException : () => {}
} as ExecutionContext ;
AI operation requires API key
Problem : operation: think fails with authentication errorFix : Add API key to wrangler.toml or environment:[ vars ]
ANTHROPIC_API_KEY = "sk-ant-..."
Or use Cloudflare Workers AI (no key needed): config :
provider : cloudflare
model : '@cf/meta/llama-3.1-8b-instruct'
Agent fails in ensemble but works in tests
Problem : Agent works when called directly but fails in ensemblesCause : Agent not using AgentExecutionContext signatureFix : Update agent signature:import type { AgentExecutionContext } from '@ensemble-edge/conductor' ;
export default function myAgent ({ input , env , ctx } : AgentExecutionContext ) {
const { params } = input as MyInput ;
// Your logic...
}
See Agent Signatures section above.
Operation type not supported
Problem : Want to use an operation that doesn’t existFix : Use operation: code and implement in TypeScript:import type { AgentExecutionContext } from '@ensemble-edge/conductor' ;
export default async function myAgent ({ input , env , ctx } : AgentExecutionContext ) {
const { data } = input as { data : string };
// Your custom logic here
const result = await customOperation ( data );
return { result };
}
Code operations can do anything TypeScript can do!
TypeScript Agent Handlers
Every agent with operation: code needs a TypeScript handler. Here’s everything you need to know about writing effective handlers.
Handler Structure
agents/user/my-agent/
├── agent.yaml # Agent configuration (declares operation: code)
└── index.ts # TypeScript handler implementation
The AgentExecutionContext
All handlers receive the same context object:
import type { AgentExecutionContext } from '@ensemble-edge/conductor'
interface AgentExecutionContext {
input : Record < string , unknown > // Input parameters
env : ConductorEnv // Cloudflare bindings (KV, D1, AI, etc.)
ctx : ExecutionContext // Cloudflare execution context
}
Handler Patterns
Simple Synchronous Handler:
import type { AgentExecutionContext } from '@ensemble-edge/conductor'
export default function greet ({ input } : AgentExecutionContext ) {
const { name } = input as { name : string }
return { message: `Hello, ${ name } !` }
}
Async Handler with External APIs:
import type { AgentExecutionContext } from '@ensemble-edge/conductor'
export default async function fetchData ({ input , env } : AgentExecutionContext ) {
const { url } = input as { url : string }
const response = await fetch ( url , {
headers: { 'Authorization' : `Bearer ${ env . API_KEY } ` }
})
const data = await response . json ()
return { data , status: response . status }
}
Handler with Cloudflare Bindings:
import type { AgentExecutionContext } from '@ensemble-edge/conductor'
export default async function cacheData ({ input , env } : AgentExecutionContext ) {
const { key , value } = input as { key : string ; value : string }
// Use KV binding
await env . CACHE . put ( key , value , { expirationTtl: 3600 })
// Use D1 database
const result = await env . DB . prepare (
'INSERT INTO cache_log (key, timestamp) VALUES (?, ?)'
). bind ( key , Date . now ()). run ()
return { cached: true , dbResult: result }
}
Using Agents in TypeScript Ensembles
Once you have YAML agents with TypeScript handlers, you can reference them in TypeScript ensembles:
// ensembles/data-pipeline.ts
import { createEnsemble , step } from '@ensemble-edge/conductor'
const dataPipeline = createEnsemble ( 'data-pipeline' )
. trigger ({ type: 'cli' , command: 'pipeline' })
. addStep (
step ( 'fetch' )
. agent ( 'fetcher' ) // References agents/user/fetcher/agent.yaml
. input ({ url: '${input.sourceUrl}' })
)
. addStep (
step ( 'process' )
. agent ( 'data-processor' ) // References agents/user/data-processor/agent.yaml
. input ({ data: '${fetch.output.data}' })
)
. addStep (
step ( 'store' )
. agent ( 'cache-writer' )
. input ({
key: '${input.cacheKey}' ,
value: '${process.output.result}'
})
)
. build ()
export default dataPipeline
Validating Agents
Validate your agent configurations:
# Validate a single agent
ensemble conductor validate agents/user/my-agent/agent.yaml
# Validate all agents recursively
ensemble conductor validate agents/ -r
Next Steps
Advanced: Versioning with Edgit (Optional)
If you want component-level versioning, you can use Edgit:
# Add agent to Edgit
edgit components add my-agent agents/user/my-agent/ agent
# Tag a version
edgit tag create my-agent v1.0.0
# Reference versioned agent
Note : Edgit is optional. Standard git version control works great!
Learn more about Edgit