Skip to main content
Think of agents as the workers in your system - each one has a specific job and does it well.

Agent Hierarchy

Components (prompts, configs)  -> Versioned artifacts
    |
Agents (workers)               -> Reusable logic
    |
Ensembles (orchestration)      -> Workflows

Anatomy of an Agent

An agent has:
  • Inputs: Parameters it accepts
  • Operation: The type of work it performs (code, think, http, etc.)
  • Handler (for code operations): TypeScript file that implements the logic
  • Outputs: Data it returns

The Handler Pattern

For custom logic, agents use the handler pattern which separates YAML contracts from TypeScript implementation: YAML (Contract) - Declares WHAT the agent does:
# agents/my-agent/my-agent.yaml
name: my-agent
operation: code
handler: ./my-agent.ts
description: What this agent does

schema:
  input:
    query: string
  output:
    result: string
TypeScript (Implementation) - Defines HOW it works:
// agents/my-agent/my-agent.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default async function handler(ctx: AgentExecutionContext) {
  const { query } = ctx.input
  return { result: `Processed: ${query}` }
}
This pattern keeps agents:
  • Testable - Pure TypeScript functions
  • Type-safe - Full TypeScript type checking
  • Debuggable - Standard debugging tools work
  • Maintainable - Clear separation of concerns

Multi-Operation Agent Example

For agents with multiple operations, use YAML flow:
# agents/company-enricher/agent.yaml
agent: company-enricher
description: Enriches company data from multiple sources

inputs:
  company_name:
    type: string
    required: true
  include_news:
    type: boolean
    default: false

operations:
  # Search for company
  - name: search
    operation: http
    config:
      url: https://api.duckduckgo.com/?q=${input.company_name}&format=json

  # Scrape website
  - name: scrape
    operation: http
    config:
      url: ${search.output.AbstractURL}

  # Extract with AI
  - name: extract
    operation: think
    config:
      provider: openai
      model: gpt-4o-mini
      prompt: |
        Extract company info from: ${scrape.output.body}
        Return JSON: {name, description, industry, founded}

  # Optional news
  - name: fetch-news
    operation: http
    condition: ${input.include_news}
    config:
      url: https://news-api.com?q=${input.company_name}

outputs:
  company_data: ${extract.output}
  news: ${fetch-news.output}
  source: ${search.output.AbstractURL}

Agent Types

Conductor provides three categories of agents:

1. System Agents

Built-in agents for core infrastructure functionality. Location: agents/system/ Available:
  • redirect - URL redirect service (permanent, expiring, single-use links)
  • docs - Documentation generation and serving
  • fetch - HTTP fetching with caching
  • validate - Data validation with multiple strategies
  • scrape - Web scraping with bot detection
  • slug - Generate URL-safe slugs
  • tools - MCP tools integration
  • queries - SQL query execution

2. Debug Agents

Development utilities for testing and debugging. Location: agents/debug/ Available:
  • echo - Returns input unchanged (inspect data)
  • delay - Add artificial delay (simulate slow operations)
  • inspect-context - View execution context

3. Custom Agents

Agents you build for your specific needs. Location: agents/user/my-agent/ Example: Company enricher, data processor, report generator

4. Pre-built Feature Agents

Ready-made agents for common workflows. Location: Built-in, reference by name Available:
  • scraper - Web scraping
  • validator - Data validation
  • rag - Retrieval-augmented generation
  • hitl - Human-in-the-loop approval
  • fetcher - Smart HTTP fetching
  • transformer - Data transformation
  • scheduler - Task scheduling
See Starter Kit for details.

Using Agents

In Ensembles

# ensembles/enrich-company.yaml
ensemble: enrich-company

agents:
  # Use custom agent
  - name: enricher
    agent: company-enricher
    inputs:
      company_name: ${input.company}
      include_news: true

  # Use pre-built agent
  - name: validate
    agent: validator
    inputs:
      data: ${enricher.output.company_data}
      schema: company-schema

output:
  data: ${enricher.output.company_data}
  valid: ${validate.output.valid}

Directly via API

const result = await conductor.executeAgent('company-enricher', {
  company_name: 'Anthropic',
  include_news: true
});

Agent Composition

Agents can use other agents:
agent: full-company-profile

inputs:
  company_name:
    type: string
    required: true

operations:
  # Use company-enricher agent
  - name: enrich
    agent: company-enricher
    inputs:
      company_name: ${input.company_name}

  # Use scraper agent
  - name: scrape-social
    agent: scraper
    inputs:
      url: https://linkedin.com/company/${input.company_name}

  # Merge results
  - name: merge
    operation: code
    config:
      script: scripts/merge-company-profiles
    input:
      company_data: ${enrich.output.company_data}
      social_data: ${scrape-social.output}

outputs:
  profile: ${merge.output}
// scripts/merge-company-profiles.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function mergeCompanyProfiles(context: AgentExecutionContext) {
  const { company_data, social_data } = context.input

  return {
    ...company_data,
    social: social_data
  }
}

Versioning Agents

Version agents with Edgit:
# Register agent
edgit components add company-enricher agents/company-enricher/ agent

# Create version
edgit tag create company-enricher v1.0.0

# Tag for production
edgit tag set company-enricher prod v1.0.0
edgit push --tags --force
Lock to specific versions in ensembles:
agents:
  - name: enricher
    agent: [email protected]  # Pinned to v1.0.0
    inputs:
      company_name: ${input.company}

Agent Features

State Management

Share state across operations:
agent: stateful-processor

state:
  schema:
    processed_count: number
    items: array

operations:
  - name: process
    operation: code
    config:
      script: scripts/process-item
    input:
      item: ${input.item}
      current_items: ${state.items || []}
    state:
      use: [items]
      set:
        items: ${process.output.items}
        processed_count: ${process.output.count}

outputs:
  count: ${state.processed_count}
// scripts/process-item.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function processItem(context: AgentExecutionContext) {
  const { item, current_items } = context.input

  return {
    items: [...current_items, item],
    count: current_items.length + 1
  }
}

Caching

Cache entire agent execution:
agent: expensive-analysis

cache:
  ttl: 3600
  key: analysis-${input.document_id}

operations:
  # ...operations...
Or cache individual operations:
operations:
  - name: scrape
    operation: http
    config:
      url: ${input.url}
    cache:
      ttl: 86400  # 24 hours

Error Handling

Graceful failure handling:
operations:
  - name: try-primary
    operation: http
    config:
      url: https://primary-api.com
    retry:
      maxAttempts: 2

  - name: fallback
    operation: http
    condition: ${try-primary.failed}
    config:
      url: https://backup-api.com

outputs:
  data: ${try-primary.output || fallback.output}
  source: ${try-primary.executed ? 'primary' : 'fallback'}

Agent Patterns

Pattern 1: Data Pipeline

agent: data-pipeline

inputs:
  raw_data: object

operations:
  - name: validate
    agent: validator
    inputs:
      data: ${input.raw_data}

  - name: transform
    agent: transformer
    condition: ${validate.output.valid}
    inputs:
      data: ${input.raw_data}

  - name: store
    operation: storage
    condition: ${validate.output.valid}
    config:
      type: d1
      query: INSERT INTO data (json) VALUES (?)
      params: [${transform.output}]

outputs:
  success: ${validate.output.valid}
  stored: ${store.executed}

Pattern 2: AI Chain

agent: content-analyzer

inputs:
  text: string

operations:
  - name: extract-entities
    operation: think
    config:
      provider: openai
      model: gpt-4o-mini
      prompt: Extract entities from: ${input.text}

  - name: analyze-sentiment
    operation: think
    config:
      provider: openai
      model: gpt-4o-mini
      prompt: Analyze sentiment: ${input.text}

  - name: summarize
    operation: think
    config:
      provider: openai
      model: gpt-4o-mini
      prompt: Summarize: ${input.text}

outputs:
  entities: ${extract-entities.output}
  sentiment: ${analyze-sentiment.output}
  summary: ${summarize.output}

Pattern 3: API Orchestration

agent: multi-source-aggregator

inputs:
  user_id: string

operations:
  # Parallel API calls
  - name: fetch-profile
    operation: http
    config:
      url: https://api1.com/users/${input.user_id}

  - name: fetch-activity
    operation: http
    config:
      url: https://api2.com/activity?user=${input.user_id}

  - name: fetch-preferences
    operation: http
    config:
      url: https://api3.com/prefs/${input.user_id}

  # Merge results
  - name: merge
    operation: code
    config:
      script: scripts/merge-user-data
    input:
      profile: ${fetch-profile.output.body}
      activity: ${fetch-activity.output.body}
      preferences: ${fetch-preferences.output.body}

outputs:
  user_data: ${merge.output}
// scripts/merge-user-data.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function mergeUserData(context: AgentExecutionContext) {
  const { profile, activity, preferences } = context.input

  return {
    profile,
    activity,
    preferences
  }
}

Testing Agents

// agents/company-enricher/agent.test.ts
import { describe, it, expect } from 'vitest';
import { TestConductor } from '@ensemble-edge/conductor/testing';

describe('company-enricher', () => {
  it('should enrich company data', async () => {
    const conductor = await TestConductor.create();
    await conductor.loadProject('./');

    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('industry');
  });

  it('should include news when requested', async () => {
    const conductor = await TestConductor.create();
    await conductor.loadProject('./');

    const result = await conductor.executeAgent('company-enricher', {
      company_name: 'Anthropic',
      include_news: true
    });

    expect(result.output.news).toBeDefined();
  });
});

Using Agents in TypeScript Ensembles

Agents defined in YAML can be used seamlessly in TypeScript ensembles:
import { createEnsemble, step, parallel } from '@anthropic/conductor'

const enrichAndAnalyze = createEnsemble('enrich-and-analyze')
  // Use custom YAML agent
  .addStep(
    step('enricher')
      .agent('company-enricher')  // References agents/company-enricher/agent.yaml
      .input({
        company_name: '${input.company}',
        include_news: true
      })
  )
  // Use pre-built agent
  .addStep(
    step('validate')
      .agent('validator')
      .input({
        data: '${enricher.output.company_data}',
        schema: 'company-schema'
      })
  )
  // Parallel agent execution
  .addStep(
    parallel('multi-scrape')
      .steps(
        step('scrape-linkedin').agent('scraper').input({ url: '${input.linkedinUrl}' }),
        step('scrape-twitter').agent('scraper').input({ url: '${input.twitterUrl}' })
      )
  )
  .build()

export default enrichAndAnalyze

Agent Composition Patterns

Compose multiple agents into reusable patterns:
import { createEnsemble, step, tryStep } from '@anthropic/conductor'

// Resilient data fetching pattern
const resilientFetch = createEnsemble('resilient-fetch')
  .addStep(
    tryStep('fetch-with-fallback')
      .try(
        step('primary')
          .agent('fetcher')
          .input({ url: '${input.primaryUrl}' })
          .retry({ maxAttempts: 2, backoff: 'exponential' })
      )
      .catch(
        step('fallback')
          .agent('fetcher')
          .input({ url: '${input.fallbackUrl}' })
      )
  )
  .build()

export default resilientFetch
For complete TypeScript API documentation, see the TypeScript API Reference.

Best Practices

  1. Single Responsibility - Each agent does one thing well
  2. Clear Inputs/Outputs - Document what goes in and comes out
  3. Version Agents - Use Edgit for version control
  4. Test Thoroughly - Unit test each agent
  5. Cache Aggressively - Cache expensive operations
  6. Handle Failures - Always have fallbacks
  7. Keep It Declarative - Let Conductor handle orchestration
  8. Monitor Performance - Track execution times

Next Steps