Skip to main content
This guide covers everything you need to run Conductor on Cloudflare’s edge platform.

Workers AI

AI models running at the edge. No API keys, no external dependencies.

Setup

Add to wrangler.toml:
[ai]
binding = "AI"
That’s it. No additional configuration.

Available Models

Text Generation:
  • @cf/meta/llama-3.1-8b-instruct - Fast, good quality (recommended)
  • @cf/meta/llama-3-8b-instruct - Previous generation
  • @cf/mistral/mistral-7b-instruct-v0.1 - Fast, creative
Embeddings:
  • @cf/baai/bge-base-en-v1.5 - 768 dimensions (recommended)
  • @cf/baai/bge-small-en-v1.5 - 384 dimensions (faster)
  • @cf/baai/bge-large-en-v1.5 - 1024 dimensions (best quality)
Image:
  • @cf/stabilityai/stable-diffusion-xl-base-1.0
Full list: https://developers.cloudflare.com/workers-ai/models/

Usage in Ensembles

agents:
  - name: generate
    operation: think
    config:
      provider: cloudflare
      model: '@cf/meta/llama-3.1-8b-instruct'
      prompt: ${input.text}

Limits

  • Free: 10,000 neurons/day (~100 requests for Llama 3.1)
  • Paid: Unlimited, pay per neuron

KV (Key-Value Storage)

Fast, globally replicated key-value store. Perfect for caching.

Create Namespace

# Create production namespace
wrangler kv:namespace create CACHE

# Create preview namespace (for local dev)
wrangler kv:namespace create CACHE --preview
Output:
{ binding = "CACHE", id = "abc123..." }
{ binding = "CACHE", preview_id = "xyz789..." }

Configure

Add to wrangler.toml:
[[kv_namespaces]]
binding = "CACHE"
id = "abc123..."
preview_id = "xyz789..."

Usage in Ensembles

agents:
  # Read from KV
  - name: get-cache
    operation: storage
    config:
      backend: kv
      action: get
      binding: CACHE
      key: result-${input.query}

  # Write to KV
  - name: set-cache
    operation: storage
    config:
      backend: kv
      action: put
      binding: CACHE
      key: result-${input.query}
      value: ${generate.output}
      expirationTtl: 3600  # 1 hour

KV Operations

Get:
config:
  type: kv
  action: get
  key: my-key
  default: null  # Return if key doesn't exist
Put:
config:
  type: kv
  action: put
  key: my-key
  value: ${data}
  expirationTtl: 3600  # Optional: expire after 1 hour
Delete:
config:
  type: kv
  action: delete
  key: my-key
List:
config:
  type: kv
  action: list
  prefix: user-  # List all keys starting with "user-"
  limit: 100

Limits

  • Free: 100,000 reads/day, 1,000 writes/day
  • Paid: Unlimited, 0.50/millionreads,0.50/million reads, 5/million writes
  • Key size: Max 512 bytes
  • Value size: Max 25MB
  • Latency: <10ms globally

D1 (SQL Database)

SQLite at the edge. Perfect for structured data.

Create Database

wrangler d1 create production-db
Output:
database_name = "production-db"
database_id = "abc123-def456-ghi789"

Configure

Add to wrangler.toml:
[[d1_databases]]
binding = "DB"
database_name = "production-db"
database_id = "abc123-def456-ghi789"

Create Schema

# Create migration
wrangler d1 migrations create production-db initial_schema
Edit migrations/0001_initial_schema.sql:
CREATE TABLE IF NOT EXISTS users (
  id TEXT PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  created_at INTEGER NOT NULL
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created ON users(created_at);

CREATE TABLE IF NOT EXISTS documents (
  id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL,
  content TEXT NOT NULL,
  embedding BLOB,
  created_at INTEGER NOT NULL,
  FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX idx_documents_user ON documents(user_id);
Apply migration:
# Apply to local dev database
wrangler d1 migrations apply production-db --local

# Apply to remote production database
wrangler d1 migrations apply production-db --remote

Usage in Ensembles

Query (SELECT):
agents:
  - name: get-user
    operation: data
    config:
      backend: d1
      binding: DB
      operation: query
      sql: |
        SELECT id, email, name FROM users
        WHERE email = ?
      params:
        - ${input.email}
Insert:
agents:
  - name: create-user
    operation: data
    config:
      backend: d1
      binding: DB
      operation: execute
      sql: |
        INSERT INTO users (id, email, name, created_at)
        VALUES (?, ?, ?, ?)
      params:
        - ${input.id}
        - ${input.email}
        - ${input.name}
        - ${Date.now()}
Update:
agents:
  - name: update-user
    operation: data
    config:
      backend: d1
      binding: DB
      operation: execute
      sql: |
        UPDATE users SET name = ?
        WHERE id = ?
      params:
        - ${input.name}
        - ${input.id}
Batch Insert:
agents:
  - name: batch-insert
    operation: data
    config:
      backend: d1
      binding: DB
      operation: batch
      statements:
        - sql: INSERT INTO users (id, email) VALUES (?, ?)
          params: ['1', '[email protected]']
        - sql: INSERT INTO users (id, email) VALUES (?, ?)
          params: ['2', '[email protected]']

D1 CLI Commands

# Execute query
wrangler d1 execute production-db --command="SELECT * FROM users LIMIT 10"

# Execute from file
wrangler d1 execute production-db --file=query.sql

# Backup database
wrangler d1 export production-db --output=backup.sql

# Restore from backup
wrangler d1 execute production-db --file=backup.sql

Limits

  • Free: 5M rows read/day, 100k rows written/day
  • Paid: Unlimited, 0.001/1krowsread,0.001/1k rows read, 1/1M rows written
  • Database size: 10GB (can request increase)
  • Query time: 30 seconds max

R2 (Object Storage)

S3-compatible object storage. No egress fees.

Create Bucket

wrangler r2 bucket create conductor-assets

Configure

Add to wrangler.toml:
[[r2_buckets]]
binding = "ASSETS"
bucket_name = "conductor-assets"

Usage in Ensembles

Upload:
agents:
  - name: upload-file
    operation: storage
    config:
      backend: r2
      action: put
      binding: STORAGE
      key: documents/${input.filename}
      value: ${input.content}
      httpMetadata:
        contentType: application/pdf
Download:
agents:
  - name: download-file
    operation: storage
    config:
      backend: r2
      action: get
      binding: STORAGE
      key: documents/${input.filename}
Delete:
agents:
  - name: delete-file
    operation: storage
    config:
      backend: r2
      action: delete
      binding: STORAGE
      key: documents/${input.filename}
List:
agents:
  - name: list-files
    operation: storage
    config:
      backend: r2
      action: list
      binding: STORAGE
      prefix: documents/
      limit: 1000

R2 CLI Commands

# Upload file
wrangler r2 object put conductor-assets/path/to/file.pdf --file=local-file.pdf

# Download file
wrangler r2 object get conductor-assets/path/to/file.pdf

# Delete file
wrangler r2 object delete conductor-assets/path/to/file.pdf

# List objects
wrangler r2 object list conductor-assets --prefix=documents/

Limits

  • Storage: 10GB free, then $0.015/GB/month
  • Class A operations (writes): 1M free/month, then $4.50/million
  • Class B operations (reads): 10M free/month, then $0.36/million
  • Egress: FREE (no egress fees!)
  • Max object size: 5TB

Vectorize (Vector Database)

Vector similarity search. Perfect for RAG and semantic search.

Create Index

wrangler vectorize create embeddings \
  --dimensions=768 \
  --metric=cosine
Dimension options:
  • 384: @cf/baai/bge-small-en-v1.5
  • 768: @cf/baai/bge-base-en-v1.5 (recommended)
  • 1024: @cf/baai/bge-large-en-v1.5
  • 1536: OpenAI text-embedding-3-small
Metric options:
  • cosine: Best for normalized vectors (recommended)
  • euclidean: L2 distance
  • dot-product: Inner product

Configure

Add to wrangler.toml:
[[vectorize]]
binding = "VECTORIZE"
index_name = "embeddings"

Usage in Ensembles

Insert Vectors:
agents:
  # Generate embedding
  - name: embed
    operation: think
    config:
      provider: cloudflare
      model: '@cf/baai/bge-base-en-v1.5'
      input: ${input.text}

  # Insert into Vectorize
  - name: insert-vector
    operation: data
    config:
      backend: vectorize
      binding: VECTORIZE
      operation: insert
      id: ${input.document_id}
      vector: ${embed.output.data[0]}
      metadata:
        text: ${input.text}
        timestamp: ${Date.now()}
Search Vectors:
agents:
  # Generate query embedding
  - name: embed-query
    operation: think
    config:
      provider: cloudflare
      model: '@cf/baai/bge-base-en-v1.5'
      input: ${input.question}

  # Search similar vectors
  - name: search
    operation: data
    config:
      backend: vectorize
      binding: VECTORIZE
      operation: query
      vector: ${embed-query.output.data[0]}
      topK: 5
      filter:
        timestamp: { $gte: ${Date.now() - 86400000} }  # Last 24 hours
Delete Vectors:
agents:
  - name: delete-vector
    operation: data
    config:
      backend: vectorize
      binding: VECTORIZE
      operation: delete
      ids:
        - ${input.document_id}

Vectorize CLI Commands

# Get index info
wrangler vectorize get embeddings

# Insert vectors (from file)
wrangler vectorize insert embeddings --file=vectors.ndjson

# Delete index
wrangler vectorize delete embeddings

Limits

  • Free: Included in Workers plan
  • Max vectors: 5M (can request increase)
  • Max dimensions: 1536
  • Query latency: ~50ms

Durable Objects

Stateful edge workers. Perfect for real-time features.

Configure

Add to wrangler.toml:
[[durable_objects.bindings]]
name = "EXECUTION_STATE"
class_name = "ExecutionState"
script_name = "my-conductor-app"

[[durable_objects.bindings]]
name = "HITL_STATE"
class_name = "HITLState"
script_name = "my-conductor-app"

[[migrations]]
tag = "v1"
new_classes = ["ExecutionState", "HITLState"]

Export Classes

In src/index.ts:
export { ExecutionState, HITLState } from '@ensemble-edge/conductor/cloudflare';

Usage

Conductor uses Durable Objects internally for:
  • HITL (Human-in-the-Loop): Approval workflows
  • Long-running executions: State persistence
You typically don’t interact with them directly.

Queues

Asynchronous task processing.

Create Queue

wrangler queues create conductor-tasks

Configure

Add to wrangler.toml:
# Producer (send messages)
[[queues.producers]]
binding = "TASK_QUEUE"
queue = "conductor-tasks"

# Consumer (receive messages)
[[queues.consumers]]
queue = "conductor-tasks"
max_batch_size = 10
max_batch_timeout = 30
max_retries = 3
dead_letter_queue = "conductor-tasks-dlq"

Usage in Ensembles

Send Message:
agents:
  - name: queue-task
    operation: queue
    config:
      action: send
      binding: TASK_QUEUE
      body:
        task_type: process-document
        document_id: ${input.document_id}
Consumer (in src/index.ts):
export default {
  async queue(batch: MessageBatch, env: Env): Promise<void> {
    for (const message of batch.messages) {
      const { task_type, document_id } = message.body;

      if (task_type === 'process-document') {
        const conductor = new Conductor({ env });
        await conductor.execute('process-document', { document_id });
      }

      message.ack();
    }
  }
};

Complete Configuration Example

Here’s a full wrangler.toml with all services:
name = "my-conductor-app"
main = "src/index.ts"
compatibility_date = "2024-01-01"
node_compat = true

# Workers AI
[ai]
binding = "AI"

# KV Namespaces
[[kv_namespaces]]
binding = "CACHE"
id = "abc123..."
preview_id = "xyz789..."

[[kv_namespaces]]
binding = "API_KEYS"
id = "def456..."

[[kv_namespaces]]
binding = "SESSIONS"
id = "ghi789..."

# D1 Databases
[[d1_databases]]
binding = "DB"
database_name = "production-db"
database_id = "jkl012..."

# R2 Buckets
[[r2_buckets]]
binding = "ASSETS"
bucket_name = "conductor-assets"

# Vectorize
[[vectorize]]
binding = "VECTORIZE"
index_name = "embeddings"

# Durable Objects
[[durable_objects.bindings]]
name = "EXECUTION_STATE"
class_name = "ExecutionState"
script_name = "my-conductor-app"

[[durable_objects.bindings]]
name = "HITL_STATE"
class_name = "HITLState"
script_name = "my-conductor-app"

[[migrations]]
tag = "v1"
new_classes = ["ExecutionState", "HITLState"]

# Queues
[[queues.producers]]
binding = "TASK_QUEUE"
queue = "conductor-tasks"

[[queues.consumers]]
queue = "conductor-tasks"
max_batch_size = 10
max_batch_timeout = 30

# Environment Variables
[vars]
ENVIRONMENT = "production"
LOG_LEVEL = "info"

# Production Environment
[env.production]
name = "my-conductor-app-prod"
vars = { ENVIRONMENT = "production" }

# Staging Environment
[env.staging]
name = "my-conductor-app-staging"
vars = { ENVIRONMENT = "staging" }

Best Practices

  1. Use KV for caching - Fast and cheap
  2. Use D1 for structured data - When you need SQL
  3. Use R2 for large files - No egress fees
  4. Use Vectorize for semantic search - Native vector support
  5. Use Queues for async work - Don’t block requests
  6. Separate environments - dev, staging, production
  7. Monitor limits - Check Cloudflare dashboard regularly
  8. Optimize queries - Add indexes to D1 tables

Cost Optimization

Stay in Free Tier

  • Workers: 100k requests/day
  • KV: 100k reads, 1k writes/day
  • D1: 5M reads, 100k writes/day
  • R2: 10GB storage, 1M writes, 10M reads/month
  • Workers AI: 10k neurons/day

Tips to Reduce Costs

  1. Cache aggressively - Use KV to cache AI/API results
  2. Use Cloudflare AI - Cheaper than external providers
  3. Batch D1 operations - Reduce write counts
  4. Use R2 for storage - No egress fees
  5. Monitor usage - Set up billing alerts

Next Steps