conductorv2

Core

Webhooks

Event-driven integrations. Push Conductor events to any endpoint, or receive external events to trigger tool calls. All outgoing webhooks are signed with HMAC-SHA256.

Overview

Conductor supports two webhook directions:

Outgoing webhooks

Conductor pushes signed event payloads to your endpoints when tools are called, plugins change state, or the circuit breaker fires. Use these for observability, alerting, and automation — pipe Conductor events into Slack, PagerDuty, Datadog, or any custom endpoint.

Incoming webhooks

Register named endpoints that external services (GitHub, Stripe, CI pipelines) can POST to. Conductor validates, logs, and routes the payload to the specified handler tool. Requires HTTP transport.

Adding a webhook

Register an outgoing webhook endpoint with the CLI. Webhooks fire for every enabled event type unless you filter with --events.

terminal
# Subscribe to all events:
conductor webhooks add --url https://hooks.example.com/conductor

# Subscribe to specific events only:
conductor webhooks add \
  --url https://hooks.example.com/conductor \
  --events tool.called,tool.failed

# Add with HMAC signing secret:
conductor webhooks add \
  --url https://hooks.example.com/conductor \
  --events tool.called,tool.failed,circuit.opened \
  --secret whsec_your_signing_secret

# List all registered webhooks:
conductor webhooks list

# Remove a webhook:
conductor webhooks remove <id>

# Send a test payload:
conductor webhooks test <id>

Event types

8 event types are available. Expand each to see the full payload schema.

tool.calledFires when a tool execution completes successfully.
example payload
{
  "id": "evt_01HXYZ",
  "type": "tool.called",
  "timestamp": "2026-04-04T10:23:45.123Z",
  "data": {
    "tool_name": "github_issues",
    "plugin": "github",
    "input": { "owner": "myorg", "repo": "api", "state": "open" },
    "output": { "count": 14, "issues": [...] },
    "latency_ms": 312,
    "session_id": "sess_abc123"
  }
}
tool.failedFires when a tool throws an error or times out.
example payload
{
  "id": "evt_01HXYZ",
  "type": "tool.failed",
  "timestamp": "2026-04-04T10:23:45.123Z",
  "data": {
    "tool_name": "slack_send",
    "plugin": "slack",
    "error": "ECONNREFUSED — Slack API unreachable",
    "error_code": "PLUGIN_REQUEST_FAILED",
    "circuit_state": "closed",
    "attempt": 3
  }
}
plugin.readyFires when a plugin initializes successfully and its tools are registered.
example payload
{
  "id": "evt_01HXYZ",
  "type": "plugin.ready",
  "timestamp": "2026-04-04T10:23:45.123Z",
  "data": {
    "plugin_name": "github",
    "tool_count": 20,
    "zero_config": false,
    "version": "1.0.0"
  }
}
plugin.errorFires when a plugin's initialize() throws or isConfigured() returns false.
example payload
{
  "id": "evt_01HXYZ",
  "type": "plugin.error",
  "timestamp": "2026-04-04T10:23:45.123Z",
  "data": {
    "plugin_name": "slack",
    "error": "Missing SLACK_BOT_TOKEN in keychain",
    "error_code": "PLUGIN_NOT_CONFIGURED",
    "state": "not_configured"
  }
}
circuit.openedFires when a circuit breaker opens after repeated tool failures.
example payload
{
  "id": "evt_01HXYZ",
  "type": "circuit.opened",
  "timestamp": "2026-04-04T10:23:45.123Z",
  "data": {
    "tool_name": "github_issues",
    "plugin": "github",
    "failure_count": 5,
    "threshold": 5,
    "reset_after_ms": 30000
  }
}
circuit.closedFires when a circuit breaker resets and the tool is available again.
example payload
{
  "id": "evt_01HXYZ",
  "type": "circuit.closed",
  "timestamp": "2026-04-04T10:23:45.123Z",
  "data": {
    "tool_name": "github_issues",
    "plugin": "github",
    "downtime_ms": 32410
  }
}
audit.writtenFires every time an entry is written to the append-only audit log.
example payload
{
  "id": "evt_01HXYZ",
  "type": "audit.written",
  "timestamp": "2026-04-04T10:23:45.123Z",
  "data": {
    "seq": 1043,
    "entry_hash": "sha256:a3f4...",
    "previous_hash": "sha256:9b2c...",
    "tool": "github_create_issue",
    "chain_intact": true
  }
}
server.startedFires once when the MCP server finishes initialization.
example payload
{
  "id": "evt_01HXYZ",
  "type": "server.started",
  "timestamp": "2026-04-04T10:23:45.123Z",
  "data": {
    "version": "2.1.0",
    "tool_count": 87,
    "plugin_count": 12,
    "transport": "stdio",
    "zero_config_plugins": 8
  }
}

Signature verification

Every outgoing webhook request includes an X-Conductor-Signature header in the format sha256=<hex_digest>. Always verify this header before processing a payload. Use your signing secret from conductor webhooks add --secret.

Node.js
const crypto = require("crypto");

function verifySignature(rawBody, signature, secret) {
  // Conductor sends: X-Conductor-Signature: sha256=<hex>
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(rawBody)         // rawBody must be the raw Buffer, not parsed JSON
    .digest("hex");

  // Use timingSafeEqual to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature, "utf8"),
    Buffer.from(expected, "utf8")
  );
}

// Express.js middleware example:
app.post("/conductor-events", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["x-conductor-signature"];
  const secret = process.env.CONDUCTOR_WEBHOOK_SECRET;

  if (!verifySignature(req.body, sig, secret)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body);
  console.log("Event:", event.type, event.data);
  res.status(200).send("ok");
});
Python
import hashlib
import hmac

def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    """
    Verify X-Conductor-Signature header.
    signature format: "sha256=<hex_digest>"
    """
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

# FastAPI example:
from fastapi import FastAPI, Request, HTTPException, Header

app = FastAPI()

@app.post("/conductor-events")
async def handle_event(
    request: Request,
    x_conductor_signature: str = Header(...)
):
    raw_body = await request.body()
    secret = os.environ["CONDUCTOR_WEBHOOK_SECRET"]

    if not verify_signature(raw_body, x_conductor_signature, secret):
        raise HTTPException(status_code=401, detail="Invalid signature")

    event = await request.json()
    print(f"Event: {event['type']}", event['data'])
    return {"ok": True}

Important: raw body required

The HMAC is computed over the raw request body bytes. Do not parse the JSON before verifying — pass the raw Buffer (Node) or bytes (Python) to the HMAC function. Any whitespace difference will cause verification to fail.

Retry behavior

Conductor retries failed webhook deliveries (non-2xx response or connection error) up to 5 times with exponential backoff. After all retries are exhausted, the event is written to the dead letter queue at ~/.conductor/dlq.json for manual inspection and replay.

AttemptDelayNotes
11 secondInitial retry
22 secondsExponential backoff
34 secondsExponential backoff
48 secondsExponential backoff
516 secondsFinal attempt

A small random jitter is added to each delay to prevent thundering herd issues when multiple webhooks are registered. Respond with any 2xx status code within 10 seconds to mark delivery as successful.

Incoming webhooks

Register a named endpoint on the Conductor HTTP server. External services POST to it, and Conductor validates, logs, and routes the payload to a handler tool. Requires HTTP transport (conductor mcp start --transport http).

terminal
conductor webhooks create \
  --name "github-events" \
  --url /hooks/github \
  --handler "process_github_event"

The endpoint becomes available at POST http://localhost:3000/hooks/github. All incoming payloads are recorded in the audit log.

Common use cases

Alert on circuit breaker open

circuit.opened

Subscribe to circuit.opened and forward to PagerDuty or Slack when a tool repeatedly fails. Automatically know when GitHub or Linear APIs are degraded.

Audit trail to external SIEM

audit.written

Subscribe to audit.written to stream every tool call and its inputs/outputs to your SIEM (Splunk, Datadog Logs, CloudWatch) for compliance.

Real-time observability dashboard

tool.called, tool.failed

Subscribe to tool.called and tool.failed to populate a custom metrics dashboard with tool call rates, latency, and error rates.

React to external events

incoming

Set up an incoming webhook for GitHub push events. When code is pushed to main, automatically trigger a Linear status update or Slack notification via Conductor tools.

Request headers

Every outgoing webhook request includes these headers:

HeaderValue
Content-Typeapplication/json
X-Conductor-Signaturesha256=<hmac_hex> (if secret configured)
X-Conductor-Eventtool.called (the event type)
X-Conductor-Deliveryevt_01HXYZ (unique delivery ID)
X-Conductor-Version2.1.0 (Conductor version)
User-AgentConductor-Webhooks/2.1.0