Skip to main content
The Webhook notifier plugin sends JSON-formatted notifications to any HTTP endpoint. It includes automatic retry logic with exponential backoff and supports custom headers for authentication.

Overview

The Webhook notifier provides:
  • Generic HTTP POST to any endpoint
  • Structured JSON payload format
  • Automatic retry with exponential backoff
  • Custom header support (authentication, etc.)
  • Smart retry logic (only retries 429 and 5xx errors)
  • Configurable retry behavior
This is the most flexible notifier plugin. Use it to integrate with any service that accepts HTTP webhooks: Discord, Microsoft Teams, custom dashboards, alerting systems, etc.

Configuration

Add the webhook notifier to your agent-orchestrator.yaml:
plugins:
  notifier:
    name: webhook
    config:
      url: https://your-endpoint.com/webhook
      headers:                    # Optional
        Authorization: Bearer ${API_TOKEN}
        X-Custom-Header: value
      retries: 2                  # Optional
      retryDelayMs: 1000          # Optional

Configuration Options

url
string
required
The HTTP endpoint to POST notifications to. Must be a valid HTTP/HTTPS URL.
The URL is validated at plugin creation. Invalid URLs will cause the plugin to fail initialization.
headers
object
Custom HTTP headers to include with every request. Useful for:
  • Authentication: Authorization: Bearer <token>
  • API keys: X-API-Key: <key>
  • Custom routing: X-Environment: production
  • Content hints: X-Webhook-Source: agent-orchestrator
Only string values are supported. Non-string values are ignored.
retries
number
default:2
Maximum number of retry attempts for failed requests. Set to 0 to disable retries.Retry behavior:
  • Only retries 429 (Too Many Requests) and 5xx (server errors)
  • 4xx errors (400, 401, 403, 404) are NOT retried (client errors are permanent)
  • Network errors are retried
retryDelayMs
number
default:1000
Initial delay in milliseconds before first retry. Uses exponential backoff:
  • Attempt 1: retryDelayMs * 2^0 = 1000ms
  • Attempt 2: retryDelayMs * 2^1 = 2000ms
  • Attempt 3: retryDelayMs * 2^2 = 4000ms
Must be non-negative. Invalid values default to 1000ms.

Payload Format

The webhook plugin sends JSON payloads with different structures based on the notification type.

Standard Notification

Sent via notify() method:
{
  "type": "notification",
  "event": {
    "id": "evt_123",
    "type": "pr.created",
    "priority": "urgent",
    "sessionId": "session-abc-123",
    "projectId": "my-app",
    "timestamp": "2026-01-15T10:30:00.000Z",
    "message": "Pull request created successfully",
    "data": {
      "prUrl": "https://github.com/owner/repo/pull/123",
      "ciStatus": "passing"
    }
  }
}

Notification with Actions

Sent via notifyWithActions() method:
{
  "type": "notification_with_actions",
  "event": { /* same as above */ },
  "actions": [
    {
      "label": "Approve",
      "url": "https://github.com/owner/repo/pull/123"
    },
    {
      "label": "Request Changes",
      "callbackEndpoint": "https://orchestrator.example.com/callback/request-changes"
    }
  ]
}

Custom Message

Sent via post() method:
{
  "type": "message",
  "message": "Custom notification text",
  "context": {
    "channel": "#custom-channel"
  }
}

Usage Examples

Discord Webhook

plugins:
  notifier:
    name: webhook
    config:
      url: https://discord.com/api/webhooks/YOUR/WEBHOOK/ID
      # Discord webhooks require transforming the payload format
Discord’s webhook format differs from the plugin’s default. You may need to use a transformation proxy or build a Discord-specific notifier plugin.

Microsoft Teams

plugins:
  notifier:
    name: webhook
    config:
      url: https://outlook.office.com/webhook/YOUR/WEBHOOK/URL
      retries: 3
      retryDelayMs: 2000

Custom Dashboard

plugins:
  notifier:
    name: webhook
    config:
      url: https://dashboard.example.com/api/notifications
      headers:
        Authorization: Bearer ${DASHBOARD_API_TOKEN}
        X-Source: agent-orchestrator
        X-Environment: production

Alerting System (PagerDuty, etc.)

plugins:
  notifier:
    name: webhook
    config:
      url: https://events.pagerduty.com/v2/enqueue
      headers:
        Authorization: Token token=${PAGERDUTY_TOKEN}
      retries: 5
      retryDelayMs: 500

No Retries (Fire and Forget)

plugins:
  notifier:
    name: webhook
    config:
      url: https://analytics.example.com/events
      retries: 0  # Don't retry failed requests

Retry Logic

What Gets Retried

  • 429 Too Many Requests - Rate limit, back off and retry
  • 500 Internal Server Error - Temporary server issue
  • 502 Bad Gateway - Upstream server issue
  • 503 Service Unavailable - Server overloaded
  • 504 Gateway Timeout - Upstream timeout
  • Network errors - Connection failures, DNS issues
  • 400 Bad Request - Invalid payload format
  • 401 Unauthorized - Missing/invalid authentication
  • 403 Forbidden - Insufficient permissions
  • 404 Not Found - Endpoint doesn’t exist
  • 422 Unprocessable Entity - Invalid data
  • Other 4xx errors - Client errors (permanent)

Exponential Backoff

Retries use exponential backoff to avoid overwhelming the server:
Attempt 0: Immediate request
Attempt 1: Wait 1000ms  (retryDelayMs * 2^0)
Attempt 2: Wait 2000ms  (retryDelayMs * 2^1)
Attempt 3: Wait 4000ms  (retryDelayMs * 2^2)
Exponential backoff helps prevent cascading failures and respects rate limits on the receiving endpoint.

Error Handling

When all retries are exhausted, the plugin throws an error:
Webhook POST failed (503): Service temporarily unavailable
The orchestrator’s reaction system can handle webhook failures:
reactions:
  - event: notification.failed
    action: log
    config:
      message: "Failed to send webhook notification after retries"

Security Best Practices

Security Checklist:
  • ✅ Use HTTPS endpoints only
  • ✅ Store sensitive tokens in environment variables
  • ✅ Validate receiving endpoint accepts your payload format
  • ✅ Use authentication headers when available
  • ✅ Rotate API tokens regularly
  • ✅ Monitor for failed webhooks (401/403 errors)
  • ✅ Different credentials per environment
  • ❌ Never commit API tokens to git
  • ❌ Don’t use unencrypted HTTP for sensitive data

Troubleshooting

The receiving endpoint rejected your payload format. Check:
  1. Does the endpoint expect the plugin’s JSON structure?
  2. Review their webhook documentation
  3. Test with curl:
curl -X POST -H 'Content-Type: application/json' \
  -d '{"type":"notification","event":{...}}' \
  YOUR_WEBHOOK_URL
  1. Consider building a transformation proxy or custom notifier plugin
Authentication failed. Check:
  1. Headers are configured correctly
  2. Token/API key is valid and not expired
  3. Token has correct permissions
  4. Environment variables are loaded properly
Test with curl:
curl -X POST \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"test":true}' \
  YOUR_WEBHOOK_URL
Connection issues:
  1. Verify the URL is accessible from your network
  2. Check firewall rules
  3. Test DNS resolution: nslookup your-endpoint.com
  4. Test connectivity: curl -v YOUR_WEBHOOK_URL
  5. Increase retries and retryDelayMs for flaky networks
Check logs for:
[notifier-webhook] No url configured — notifications will be no-ops
Ensure url is set in config and validated successfully.
If retries are being exhausted:
  1. Check if errors are 4xx (not retryable)
  2. Increase retries count
  3. Increase retryDelayMs for better spacing
  4. Monitor server logs on receiving end

Building a Transformation Proxy

If your webhook endpoint expects a different payload format, create a transformation proxy:
// proxy.ts - Transform to Discord format
app.post('/webhook/discord', async (req, res) => {
  const { type, event, message } = req.body;
  
  // Transform to Discord webhook format
  const discordPayload = {
    content: message || event?.message,
    embeds: event ? [{
      title: `${event.type}${event.sessionId}`,
      description: event.message,
      color: event.priority === 'urgent' ? 0xff0000 : 0x0099ff,
      timestamp: event.timestamp
    }] : []
  };
  
  await fetch(process.env.DISCORD_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(discordPayload)
  });
  
  res.status(200).send('OK');
});
Then configure the webhook plugin to use your proxy:
plugins:
  notifier:
    name: webhook
    config:
      url: https://your-proxy.com/webhook/discord

Source Code

View the plugin source:
  • Package: @composio/ao-plugin-notifier-webhook
  • Location: packages/plugins/notifier-webhook/src/index.ts

Build docs developers (and LLMs) love