Creating Agents
Build custom agents that do exactly what you need. This guide covers the full agent development lifecycle. Already read Your First Agent? This goes deeper.Agent Structure
Copy
agents/
my-agent/
agent.yaml # Agent definition
agent.test.ts # Tests
README.md # Documentation (optional)
Complete Agent Example
Copy
# agents/company-enricher/agent.yaml
agent: company-enricher
description: Enriches company data from multiple sources with fallbacks
inputs:
company_name:
type: string
required: true
description: Company name to enrich
include_news:
type: boolean
default: false
description: Whether to include recent news
cache:
ttl: 86400 # Cache entire agent for 24 hours
key: enrich-${input.company_name}
operations:
# Step 1: Search for company
- name: search
operation: http
config:
url: https://api.duckduckgo.com/?q=${input.company_name}+official+website&format=json
method: GET
cache:
ttl: 86400
key: search-${input.company_name}
retry:
maxAttempts: 3
backoff: exponential
initialDelay: 1000
# Step 2: Scrape website
- name: scrape-primary
operation: http
config:
url: ${search.output.AbstractURL}
method: GET
timeout: 10000
retry:
maxAttempts: 2
# Step 3: Fallback scrape if primary fails
- name: scrape-fallback
condition: ${scrape-primary.failed}
operation: http
config:
url: https://www.crunchbase.com/organization/${input.company_name}
method: GET
# Step 4: Extract with AI
- name: extract
operation: think
config:
provider: openai
model: gpt-4o-mini
temperature: 0.3
maxTokens: 500
prompt: |
Extract company info from this HTML:
${scrape-primary.output?.body || scrape-fallback.output?.body}
Return JSON with:
{
"name": "Company name",
"description": "Brief description (1-2 sentences)",
"industry": "Primary industry",
"founded": "Year founded if available"
}
cache:
ttl: 3600
key: extract-${input.company_name}
# Step 5: Optionally fetch news
- name: fetch-news
condition: ${input.include_news && extract.executed}
operation: http
config:
url: https://newsapi.org/v2/everything?q=${input.company_name}&sortBy=publishedAt&pageSize=5
headers:
Authorization: Bearer ${env.NEWS_API_KEY}
cache:
ttl: 3600
# Step 6: Store in cache
- name: cache-result
condition: ${extract.executed}
operation: storage
config:
type: kv
action: put
key: company-${input.company_name}
value:
company_data: ${extract.output}
news: ${fetch-news.output?.body?.articles}
cached_at: ${Date.now()}
expirationTtl: 86400
outputs:
company_data: ${extract.output}
news: ${fetch-news.output?.body?.articles}
source_url: ${search.output.AbstractURL}
from_cache: ${__cache_hit}
Caching Strategies
Agent-Level Caching
Cache the entire agent execution:Copy
agent: expensive-agent
cache:
ttl: 3600
key: expensive-${input.id}
operations:
# All operations run if cache miss
# Skip all if cache hit
Operation-Level Caching
Cache specific operations:Copy
operations:
- name: expensive-scrape
operation: http
config:
url: ${input.url}
cache:
ttl: 86400 # 24 hours
key: scrape-${input.url}
- name: ai-analysis
operation: think
config:
prompt: Analyze: ${expensive-scrape.output}
cache:
ttl: 3600 # 1 hour
key: analyze-${input.url}
Dynamic Cache Keys
Copy
operations:
- name: cached-op
operation: think
config:
prompt: ${input.text}
cache:
ttl: ${input.cache_duration || 3600}
key: ${input.cache_key || `default-${input.text}`}
Retry Logic
Basic Retry
Copy
operations:
- name: flaky-api
operation: http
config:
url: https://api.example.com
retry:
maxAttempts: 3
backoff: exponential
initialDelay: 1000
Advanced Retry
Copy
operations:
- name: smart-retry
operation: http
config:
url: https://api.example.com
retry:
maxAttempts: 5
backoff: exponential # 1s, 2s, 4s, 8s, 16s
initialDelay: 1000
maxDelay: 30000 # Cap at 30s
retryOn: [500, 502, 503, 504] # Only retry these codes
timeout: 10000 # Timeout per attempt
Conditional Retry
Copy
operations:
- name: try-operation
operation: http
config:
url: https://api.example.com
retry:
maxAttempts: 2
- name: check-retry-count
operation: code
config:
code: |
return {
should_retry: ${try-operation.retry_count} < 3,
count: ${try-operation.retry_count}
};
Error Handling
Fallback Pattern
Copy
operations:
- name: primary
operation: http
config:
url: https://primary-api.com
retry:
maxAttempts: 2
- name: secondary
condition: ${primary.failed}
operation: http
config:
url: https://backup-api.com
- name: cached
condition: ${primary.failed && secondary.failed}
operation: storage
config:
type: kv
action: get
key: fallback-data
outputs:
data: ${primary.output || secondary.output || cached.output}
source: ${primary.executed ? 'primary' : secondary.executed ? 'secondary' : 'cache'}
Error Logging
Copy
operations:
- name: risky-operation
operation: http
config:
url: https://api.example.com
- name: log-error
condition: ${risky-operation.failed}
operation: storage
config:
type: d1
query: |
INSERT INTO error_log (agent, operation, error, timestamp)
VALUES (?, ?, ?, ?)
params:
- company-enricher
- risky-operation
- ${risky-operation.error}
- ${Date.now()}
Graceful Degradation
Copy
operations:
- name: enhanced-analysis
operation: think
config:
provider: openai
model: gpt-4o
prompt: ${input.text}
- name: basic-analysis
condition: ${enhanced-analysis.failed}
operation: think
config:
provider: cloudflare
model: '@cf/meta/llama-3.1-8b-instruct'
prompt: ${input.text}
outputs:
analysis: ${enhanced-analysis.output || basic-analysis.output}
quality: ${enhanced-analysis.executed ? 'enhanced' : 'basic'}
Testing Agents
Unit Tests
Copy
// agents/company-enricher/agent.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { TestConductor } from '@ensemble-edge/conductor/testing';
describe('company-enricher agent', () => {
let conductor: TestConductor;
beforeEach(async () => {
conductor = await TestConductor.create();
await conductor.loadProject('./');
});
it('should enrich company data', async () => {
const result = await conductor.executeAgent('company-enricher', {
company_name: 'Anthropic',
include_news: false
});
expect(result).toBeSuccessful();
expect(result.output.company_data).toHaveProperty('name');
expect(result.output.company_data).toHaveProperty('description');
expect(result.output.company_data).toHaveProperty('industry');
});
it('should include news when requested', async () => {
const result = await conductor.executeAgent('company-enricher', {
company_name: 'Anthropic',
include_news: true
});
expect(result.output.news).toBeDefined();
expect(Array.isArray(result.output.news)).toBe(true);
});
it('should use cache on second call', async () => {
// First call
const result1 = await conductor.executeAgent('company-enricher', {
company_name: 'Anthropic'
});
// Second call
const result2 = await conductor.executeAgent('company-enricher', {
company_name: 'Anthropic'
});
expect(result1.output.from_cache).toBe(false);
expect(result2.output.from_cache).toBe(true);
});
it('should handle scraping failures with fallback', async () => {
// Mock primary scrape failure
conductor.mockOperation('scrape-primary', {
failed: true,
error: 'Timeout'
});
const result = await conductor.executeAgent('company-enricher', {
company_name: 'Anthropic'
});
expect(result).toBeSuccessful();
expect(result.operations['scrape-fallback'].executed).toBe(true);
});
it('should retry failed operations', async () => {
conductor.mockOperation('search', {
failed: true,
retry_count: 2
});
const result = await conductor.executeAgent('company-enricher', {
company_name: 'Anthropic'
});
expect(result.operations.search.retry_count).toBeGreaterThan(0);
});
});
Integration Tests
Copy
describe('company-enricher integration', () => {
it('should work end-to-end with real APIs', async () => {
const conductor = await TestConductor.create({
env: {
NEWS_API_KEY: process.env.NEWS_API_KEY
}
});
await conductor.loadProject('./');
const result = await conductor.executeAgent('company-enricher', {
company_name: 'Anthropic',
include_news: true
});
expect(result).toBeSuccessful();
expect(result.output.company_data.name).toContain('Anthropic');
expect(result.output.news.length).toBeGreaterThan(0);
expect(result.output.source_url).toMatch(/https?:\/\/.+/);
});
});
Mock Data
Copy
describe('company-enricher with mocks', () => {
it('should handle mocked responses', async () => {
const conductor = await TestConductor.create();
await conductor.loadProject('./');
// Mock search result
conductor.mockOperation('search', {
output: {
AbstractURL: 'https://anthropic.com'
}
});
// Mock scrape result
conductor.mockOperation('scrape-primary', {
output: {
body: '<html><body>Anthropic is an AI safety company...</body></html>'
}
});
const result = await conductor.executeAgent('company-enricher', {
company_name: 'Anthropic'
});
expect(result).toBeSuccessful();
});
});
Performance Optimization
Parallel Operations
Copy
operations:
# These run in parallel (no dependencies)
- name: fetch-a
operation: http
config:
url: https://api-a.com
- name: fetch-b
operation: http
config:
url: https://api-b.com
- name: fetch-c
operation: http
config:
url: https://api-c.com
# This waits for all 3
- name: merge
operation: code
config:
code: |
return {
a: ${fetch-a.output},
b: ${fetch-b.output},
c: ${fetch-c.output}
};
Minimize Dependencies
Copy
# Bad: Sequential
operations:
- name: step1
operation: http
config:
url: https://api.com/step1
- name: step2
operation: http
config:
url: https://api.com/step2
data: ${step1.output} # Creates dependency
# Good: Parallel
operations:
- name: step1
operation: http
config:
url: https://api.com/step1
- name: step2
operation: http
config:
url: https://api.com/step2
- name: combine
operation: code
config:
code: return { ...${step1.output}, ...${step2.output} };
Early Termination
Copy
operations:
# Quick check first
- name: quick-check
operation: code
config:
code: return { valid: ${input.data}.length > 0 };
# Only run expensive ops if valid
- name: expensive-analysis
condition: ${quick-check.output.valid}
operation: think
config:
provider: openai
model: gpt-4o
prompt: ${input.data}
Best Practices
- Single Responsibility - One agent, one job
- Cache Aggressively - Cache expensive operations
- Handle Failures - Always have fallbacks
- Use Retry Logic - For transient failures
- Test Thoroughly - Unit and integration tests
- Document Inputs/Outputs - Clear schemas
- Monitor Performance - Track execution times
- Version with Edgit - Track changes over time

