Skip to main content

Overview

Conductor supports webhooks in two directions:
  1. Inbound Webhooks - Trigger ensembles by calling a webhook URL
  2. 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).

Configure Inbound Webhook

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.

Configure Notifications

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.

Headers

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. 1 second - First retry
  2. 5 seconds - Second retry
  3. 30 seconds - Third retry
  4. 2 minutes - Fourth retry
  5. 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.

Configure Email Notifications

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