Overview
Conductor supports webhooks in two directions:
- Inbound Webhooks - Trigger ensembles by calling a webhook URL
- Outbound Notifications - Send events from ensembles to your webhooks
Inbound Webhooks
Expose ensembles as webhook endpoints that can be triggered by external services.
You own your webhook paths. You can define any path for your webhooks (e.g., /github/events, /stripe/payments, /my-custom-path). We recommend using /webhooks/* paths for clarity and consistency (e.g., /webhooks/github, /webhooks/stripe).
name: github-pr-review
trigger:
- type: webhook
path: /webhooks/github/pr-review # Recommended: /webhooks/* prefix
methods: [POST]
public: false
auth:
type: bearer
secret: ${env.GITHUB_WEBHOOK_SECRET}
flow:
- agent: review-pr
agents:
- name: review-pr
operation: api
config:
# ... analyze PR
outputs:
analysis: ${review-pr.output}
Trigger Inbound Webhook
curl -X POST https://your-worker.workers.dev/webhooks/github/pr-review \
-H "Authorization: Bearer ${GITHUB_WEBHOOK_SECRET}" \
-H "Content-Type: application/json" \
-d '{
"action": "opened",
"pull_request": {
"number": 123,
"title": "Add new feature"
}
}'
Response:
{
"executionId": "exec-abc123...",
"status": "completed",
"output": {
"analysis": "LGTM",
"score": 95
},
"metrics": {
"totalDuration": 1234,
"agents": 3
}
}
Authentication Options
Bearer Token:
trigger:
- type: webhook
path: /webhooks/secure-endpoint
auth:
type: bearer
secret: ${env.WEBHOOK_TOKEN}
HMAC Signature:
trigger:
- type: webhook
path: /webhooks/github
auth:
type: signature
secret: ${env.GITHUB_SECRET}
When using HMAC signature auth, the sender must include these headers:
X-Webhook-Signature: sha256=abc123def456...
X-Webhook-Timestamp: 1705315200
The signature is computed as:
const payload = `${timestamp}.${body}`;
const signature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
OAuth (coming soon):
trigger:
- type: webhook
path: /webhooks/oauth-endpoint
auth:
type: oauth
issuer: https://auth.example.com
audience: https://api.example.com
Basic Authentication:
trigger:
- type: webhook
path: /webhooks/basic-auth
auth:
type: basic
secret: ${env.BASIC_AUTH_CREDENTIALS} # Format: username:password
Public (No Auth):
trigger:
- type: webhook
path: /webhooks/public
public: true # Explicitly allow anonymous access
Outbound Notifications
Send notifications when events occur during ensemble execution.
name: user-onboarding
notifications:
# Webhook notification
- type: webhook
url: https://your-api.example.com/webhooks/conductor
events:
- execution.started
- execution.completed
- execution.failed
secret: ${env.WEBHOOK_SECRET}
retries: 3
timeout: 5000
# Email notification
- type: email
to:
- [email protected]
- [email protected]
from: [email protected]
subject: "Ensemble ${ensemble.name}: ${event}"
events:
- execution.failed
- execution.timeout
flow:
- agent: onboard-user
agents:
- name: onboard-user
operation: think
config:
# ... your agent config
outputs:
userId: ${onboard-user.output.userId}
Notification 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": "[email protected]"
}
}
}
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": {
"message": "Database connection failed"
},
"duration": 567
}
}
execution.timeout
Triggered when execution times out.
{
"event": "execution.timeout",
"timestamp": "2024-01-15T10:35:00Z",
"data": {
"id": "exec-abc123...",
"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 webhook notifications include a signature for verification.
X-Conductor-Signature: sha256=abc123def456...
X-Conductor-Timestamp: 1705315200
X-Conductor-Event: execution.completed
X-Conductor-Delivery-Attempt: 1
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 (5 min window)
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 (timing-safe)
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 notification
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 (timing-safe)
if not hmac.compare_digest(signature, expected_signature):
raise ValueError('Invalid webhook signature')
return True
Handling Notifications
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);
await sendAlert(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 processNotification(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 webhook notifications with exponential backoff:
- 1 second - First retry
- 5 seconds - Second retry
- 30 seconds - Third retry
- 2 minutes - Fourth retry
- 5 minutes - Final retry
Total attempts: Up to 3 retries (configurable)
Status codes that trigger retry:
5xx - Server errors
408 - Request Timeout
- Connection errors / timeouts
Status codes that don’t retry:
2xx, 3xx - Success
4xx - Client errors (except 408)
Email Notifications
Send email alerts for ensemble events.
name: critical-workflow
notifications:
- type: email
to:
- [email protected]
- [email protected]
from: [email protected]
subject: "[${event}] ${ensemble.name}"
events:
- execution.failed
- execution.timeout
flow:
- agent: critical-task
agents:
- name: critical-task
operation: think
config:
# ... your agent config
outputs:
result: ${critical-task.output}
Subject Template Variables
${event} - Event type (e.g., “execution.failed”)
${ensemble.name} - Ensemble name
${timestamp} - Event timestamp
Email Content
Emails include both plain text and HTML versions:
- Plain Text: JSON-formatted event data
- HTML: Styled template with color-coded event types
- Green: execution.completed
- Red: execution.failed, execution.timeout
- Blue: Other events
Testing Webhooks
Local Testing with ngrok
# Start ngrok
ngrok http 3000
# Test inbound webhook
curl -X POST https://abc123.ngrok.io/webhooks/test \
-H "Authorization: Bearer test-token" \
-H "Content-Type: application/json" \
-d '{"test": true}'
Test Notification Delivery
# Send test notification 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)" \
-H "X-Conductor-Event: execution.completed" \
-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 configure secrets for notifications
notifications:
- type: webhook
url: https://api.example.com/webhooks
secret: ${env.WEBHOOK_SECRET} # Required for security
// Always verify before processing
verifyWebhookSignature(req, secret);
2. Respond Quickly
// Respond within 5 seconds to avoid timeouts
res.status(200).json({ received: true });
// Process async
queueProcessor.add(event, data);
3. Handle Idempotency
// Store processed notification IDs
const processed = await db.notifications.findOne({
id: data.id,
event: event
});
if (processed) {
return res.status(200).json({ received: true });
}
// Process and store
await processNotification(event, data);
await db.notifications.insert({
id: data.id,
event,
processedAt: new Date()
});
4. Log All Notifications
await db.webhooks.insert({
id: data.id,
event,
data,
receivedAt: new Date(),
attempt: req.headers['x-conductor-delivery-attempt']
});
5. Monitor Failures
const attempt = parseInt(req.headers['x-conductor-delivery-attempt']);
if (attempt > 1) {
console.warn(`Retry attempt ${attempt} for ${event}`);
}
// Alert on persistent failures
if (recentFailures > 3) {
await alerts.notify('Webhook failures detected', {
event,
failures: recentFailures
});
}
6. Use Default-Deny Security
# GOOD: Explicit authentication
trigger:
- type: webhook
path: /webhooks/secure
auth:
type: bearer
secret: ${env.SECRET}
# GOOD: Explicitly public
trigger:
- type: webhook
path: /webhooks/public
public: true
# BAD: No auth, not marked public (will be rejected)
trigger:
- type: webhook
path: /webhooks/unsafe # Error: requires auth or public: true
Next Steps