Webhooks

HTTP callbacks for async execution completion

Webhooks

Reliable callback delivery with HMAC signatures and automatic retries

HTTP callbacks that notify your application when async executions complete. Agentfield POSTs execution results to your endpoint with HMAC-SHA256 signatures for verification.

Configuration

Register a webhook when queuing async execution:

curl -X POST http://localhost:8080/api/v1/execute/async/research-agent.analyze_market \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "market": "autonomous software",
      "depth": "comprehensive"
    },
    "webhook": {
      "url": "https://app.example.com/research/complete",
      "secret": "your-webhook-secret",
      "headers": {
        "X-Custom-ID": "research-123"
      }
    }
  }'

Response:

{
  "execution_id": "exec_abc123",
  "workflow_id": "wf_xyz789",
  "run_id": "run_def456",
  "status": "queued",
  "webhook_registered": true,
  "webhook_error": null
}

Webhook Payload

When execution completes, Agentfield POSTs to your webhook URL:

{
  "event": "execution.completed",
  "execution_id": "exec_abc123",
  "workflow_id": "wf_xyz789",
  "status": "succeeded",
  "target": "research-agent.analyze_market",
  "type": "reasoner",
  "duration_ms": 45230,
  "result": {
    "market_size": "$2.3B",
    "growth_rate": "47% YoY",
    "key_players": [...],
    "recommendation": "high_potential"
  },
  "timestamp": "2024-01-15T10:31:30Z"
}

Failed deliveries use the same shape but include error_message:

{
  "event": "execution.failed",
  "execution_id": "exec_def456",
  "workflow_id": "wf_xyz789",
  "status": "failed",
  "target": "research-agent.analyze_market",
  "type": "reasoner",
  "error_message": "Upstream service unavailable",
  "timestamp": "2024-01-15T10:31:30Z"
}

Event Types:

  • execution.completed — Emitted when the execution finished successfully
  • execution.failed — Emitted for any non-successful terminal state (failed, cancelled, timeout, etc.). Inspect the status and optional error_message fields for details.

Prop

Type

Signature Verification

Agentfield signs webhook payloads with HMAC-SHA256 whenever you provide a webhook secret. Verify signatures to ensure requests are authentic.

Signature Header:

X-Agentfield-Signature: sha256=a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3

Verification Examples:

import hmac
import hashlib

def verify_webhook(request):
    signature = request.headers.get('X-Agentfield-Signature', '')
    if not signature.startswith('sha256='):
        return False

    expected_sig = signature[7:]  # Remove 'sha256=' prefix
    secret = os.getenv('WEBHOOK_SECRET').encode()
    body = request.get_data()

    computed_sig = hmac.new(
        secret,
        body,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(computed_sig, expected_sig)

@app.route('/webhook', methods=['POST'])
async def handle_webhook():
    if not verify_webhook(request):
        return {'error': 'Invalid signature'}, 401

    payload = request.json
    # Process webhook...
import crypto from 'crypto';

function verifyWebhook(req) {
  const signature = req.headers['x-agentfield-signature'] || '';
  if (!signature.startsWith('sha256=')) {
    return false;
  }

  const expectedSig = signature.substring(7);
  const secret = process.env.WEBHOOK_SECRET;
  const body = JSON.stringify(req.body);

  const computedSig = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(computedSig),
    Buffer.from(expectedSig)
  );
}

app.post('/webhook', (req, res) => {
  if (!verifyWebhook(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const payload = req.body;
  // Process webhook...
});
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "strings"
)

func verifyWebhook(signature string, body []byte, secret string) bool {
    if !strings.HasPrefix(signature, "sha256=") {
        return false
    }

    expectedSig := signature[7:]
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    computedSig := hex.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(computedSig), []byte(expectedSig))
}

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-Agentfield-Signature")
    body, _ := io.ReadAll(r.Body)

    if !verifyWebhook(signature, body, os.Getenv("WEBHOOK_SECRET")) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    // Process webhook...
}

Security Best Practices:

  • Always verify signatures before processing webhooks
  • Treat webhooks without X-Agentfield-Signature as unauthenticated (no secret was supplied)
  • Use environment variables for webhook secrets
  • Use hmac.compare_digest() or crypto.timingSafeEqual() to prevent timing attacks
  • Validate payload structure before processing
  • Log webhook failures for debugging

Delivery Guarantees

Agentfield retries failed deliveries with exponential backoff. Defaults come from agentfield.execution_queue in agentfield.yaml and can be tuned.

  • Max Attempts: 3 total attempts by default (initial send + retries)
  • Backoff: Starts at 1s and doubles each retry up to 5s by default
  • Timeout: 10 seconds per attempt
  • Retry policy: Any non-2xx response or transport error is retried until attempts are exhausted

Webhook Event Logs:

Agentfield stores webhook delivery attempts in the database. Check execution details in the UI to see:

  • Delivery status (delivered/failed)
  • HTTP status code
  • Response body (truncated)
  • Retry attempts