Skip to main content

Webhooks

Receive real-time notifications for ensemble events.

Overview

Webhooks allow your application to receive notifications when events occur in Conductor. Instead of polling for updates, Conductor pushes events to your endpoint.

Configure Webhook

Via YAML

ensemble: user-onboarding

webhook:
  url: https://your-api.example.com/webhooks/conductor
  events:
    - execution.started
    - execution.completed
    - execution.failed
  secret: ${env.WEBHOOK_SECRET}
  retries: 3
  timeout: 5000

agents:
  # ... your agents

Via API

curl -X POST https://api.ensemble.dev/v1/webhooks \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -d '{
    "url": "https://your-api.example.com/webhooks/conductor",
    "events": ["execution.completed", "execution.failed"],
    "secret": "your-webhook-secret"
  }'
Response:
{
  "id": "webhook_abc123",
  "url": "https://your-api.example.com/webhooks/conductor",
  "events": ["execution.completed", "execution.failed"],
  "createdAt": 1705315200000
}

Webhook Events

execution.started

Triggered when execution begins.
{
  "event": "execution.started",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "id": "exec-abc123...",
    "ensemble": "user-onboarding",
    "input": {
      "email": "alice@example.com"
    }
  }
}

execution.completed

Triggered when execution completes successfully.
{
  "event": "execution.completed",
  "timestamp": "2024-01-15T10:30:15Z",
  "data": {
    "id": "exec-abc123...",
    "ensemble": "user-onboarding",
    "status": "completed",
    "output": {
      "userId": "user_xyz789",
      "created": true
    },
    "duration": 1234
  }
}

execution.failed

Triggered when execution fails.
{
  "event": "execution.failed",
  "timestamp": "2024-01-15T10:30:15Z",
  "data": {
    "id": "exec-abc123...",
    "ensemble": "user-onboarding",
    "status": "failed",
    "error": {
      "code": "AGENT_FAILED",
      "message": "Database connection failed",
      "agent": "create-account"
    },
    "duration": 567
  }
}

execution.timeout

Triggered when execution times out.
{
  "event": "execution.timeout",
  "timestamp": "2024-01-15T10:35:00Z",
  "data": {
    "id": "exec-abc123...",
    "ensemble": "user-onboarding",
    "duration": 30000,
    "timeout": 30000
  }
}

agent.completed

Triggered when individual agent completes.
{
  "event": "agent.completed",
  "timestamp": "2024-01-15T10:30:10Z",
  "data": {
    "executionId": "exec-abc123...",
    "agent": "create-account",
    "output": {
      "userId": "user_xyz789"
    },
    "duration": 234
  }
}

state.updated

Triggered when state changes.
{
  "event": "state.updated",
  "timestamp": "2024-01-15T10:30:12Z",
  "data": {
    "executionId": "exec-abc123...",
    "state": {
      "step": 3,
      "processed": 5
    }
  }
}

Signature Verification

All webhooks include a signature for verification.
X-Conductor-Signature: sha256=abc123def456...
X-Conductor-Timestamp: 1705315200

Verify Signature (Node.js)

const crypto = require('crypto');

function verifyWebhookSignature(req, secret) {
  const signature = req.headers['x-conductor-signature'];
  const timestamp = req.headers['x-conductor-timestamp'];
  const body = JSON.stringify(req.body);
  
  // Prevent replay attacks
  const currentTime = Math.floor(Date.now() / 1000);
  if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
    throw new Error('Webhook timestamp too old');
  }
  
  // Compute expected signature
  const payload = `${timestamp}.${body}`;
  const expectedSignature = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  
  // Compare signatures
  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )) {
    throw new Error('Invalid webhook signature');
  }
  
  return true;
}

// Express middleware
app.post('/webhooks/conductor', (req, res) => {
  try {
    verifyWebhookSignature(req, process.env.WEBHOOK_SECRET);
    
    // Process webhook
    const { event, data } = req.body;
    console.log(`Received ${event}:`, data);
    
    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook verification failed:', error);
    res.status(401).json({ error: error.message });
  }
});

Verify Signature (Python)

import hmac
import hashlib
import time

def verify_webhook_signature(request, secret):
    signature = request.headers.get('X-Conductor-Signature')
    timestamp = request.headers.get('X-Conductor-Timestamp')
    body = request.get_data(as_text=True)
    
    # Prevent replay attacks
    current_time = int(time.time())
    if abs(current_time - int(timestamp)) > 300:
        raise ValueError('Webhook timestamp too old')
    
    # Compute expected signature
    payload = f"{timestamp}.{body}"
    expected_signature = 'sha256=' + hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()
    
    # Compare signatures
    if not hmac.compare_digest(signature, expected_signature):
        raise ValueError('Invalid webhook signature')
    
    return True

Handling Webhooks

Basic Handler

app.post('/webhooks/conductor', async (req, res) => {
  // Verify signature
  verifyWebhookSignature(req, process.env.WEBHOOK_SECRET);
  
  const { event, data } = req.body;
  
  // Handle event
  switch (event) {
    case 'execution.completed':
      await handleExecutionCompleted(data);
      break;
    
    case 'execution.failed':
      await handleExecutionFailed(data);
      break;
    
    default:
      console.log(`Unhandled event: ${event}`);
  }
  
  // Respond quickly
  res.status(200).json({ received: true });
});

Async Processing

app.post('/webhooks/conductor', async (req, res) => {
  // Verify and respond quickly
  verifyWebhookSignature(req, process.env.WEBHOOK_SECRET);
  res.status(200).json({ received: true });
  
  // Process asynchronously
  const { event, data } = req.body;
  
  // Queue for processing
  await queue.add('webhook', { event, data });
});

Error Handling

app.post('/webhooks/conductor', async (req, res) => {
  try {
    verifyWebhookSignature(req, process.env.WEBHOOK_SECRET);
    
    const { event, data } = req.body;
    await processWebhook(event, data);
    
    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);
    
    // Return 5xx to trigger retry
    res.status(500).json({ error: 'Processing failed' });
  }
});

Retry Logic

Conductor retries failed webhooks with exponential backoff:
  1. Immediate
  2. 1 minute
  3. 5 minutes
  4. 30 minutes
  5. 2 hours
Total attempts: 5 Status codes that trigger retry:
  • 5xx - Server errors
  • 408 - Timeout
  • Connection errors
Status codes that don’t retry:
  • 2xx - Success
  • 4xx - Client errors (except 408)

Testing Webhooks

Local Testing with ngrok

# Start ngrok
ngrok http 3000

# Use ngrok URL
curl -X POST https://api.ensemble.dev/v1/webhooks \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/conductor",
    "events": ["execution.completed"]
  }'

Test Payload

# Send test webhook
curl -X POST https://your-api.example.com/webhooks/conductor \
  -H "Content-Type: application/json" \
  -H "X-Conductor-Signature: sha256=..." \
  -H "X-Conductor-Timestamp: $(date +%s)" \
  -d '{
    "event": "execution.completed",
    "timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",
    "data": {
      "id": "exec-test123",
      "ensemble": "test-ensemble",
      "status": "completed"
    }
  }'

Best Practices

1. Verify Signatures
// Always verify before processing
verifyWebhookSignature(req, secret);
2. Respond Quickly
// Respond within 5 seconds
res.status(200).json({ received: true });

// Process async
queueProcessor.add(event, data);
3. Handle Idempotency
// Store processed webhook IDs
const processed = await db.webhooks.findOne({ id: data.id });
if (processed) {
  return res.status(200).json({ received: true });
}
4. Log All Webhooks
await db.webhooks.insert({
  id: data.id,
  event,
  data,
  receivedAt: new Date()
});
5. Monitor Failures
if (failures > 3) {
  await alerts.notify('Webhook failures detected', { event, failures });
}

Next Steps