TL;DR:
- Webhook-driven agents replace polling loops with immediate event responses, dramatically reducing latency and compute waste
- Reliable webhook handling requires idempotency keys, signature verification, and a queue buffer between the webhook receiver and the agent executor
- The main failure modes — duplicate delivery, out-of-order events, and payload size limits — each have standard solutions worth building once
Most agent pipelines start with a scheduler: run every five minutes, check if something happened, act if it did. This works, but it’s wasteful. A sales agent polling a CRM every five minutes burns 288 LLM calls per day to handle maybe three new leads. A support agent checking email every two minutes wakes 720 times to handle 40 tickets. The latency is also real — worst case, a customer waits five minutes for an automated acknowledgement.
Webhook-driven architecture inverts this. Instead of asking “has anything happened?”, you build a listener that answers when something does. Events are delivered the moment they occur. The agent executes in seconds, not minutes.
The Core Architecture
A webhook-driven agent pipeline has three layers:
1. Receiver: A lightweight HTTP endpoint that accepts incoming webhook payloads, validates the signature, and immediately returns 200 OK. This must be fast — webhook senders typically have short timeout windows (5-30 seconds) and will retry or discard if you don’t respond quickly.
2. Queue: A message queue (Redis, SQS, BullMQ, Inngest) that holds the webhook payload until the agent is ready to process it. This decouples receipt from execution, handling bursts and ensuring you don’t lose events if your agent is momentarily slow.
3. Agent executor: The actual agent that reads from the queue, processes the event, and takes action. This is where your LLM calls happen.
# Minimal FastAPI webhook receiver
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
import hmac, hashlib, json
from redis import Redis
app = FastAPI()
redis_client = Redis()
WEBHOOK_SECRET = "your-webhook-secret"
@app.post("/webhook/github")
async def receive_github_webhook(
request: Request,
background_tasks: BackgroundTasks
):
# Verify signature immediately
body = await request.body()
sig = request.headers.get("X-Hub-Signature-256", "")
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET.encode(), body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(sig, expected):
raise HTTPException(status_code=401, detail="Invalid signature")
# Enqueue and return 200 fast
payload = json.loads(body)
redis_client.lpush("webhook_queue", json.dumps({
"source": "github",
"event": request.headers.get("X-GitHub-Event"),
"payload": payload,
"received_at": time.time()
}))
return {"status": "queued"}
Signature Verification: Do This First
Every major webhook provider sends a signature with each delivery — GitHub uses HMAC-SHA256, Stripe uses a timestamp-prefixed signature, Slack uses v0 signatures. Always verify before processing. Unverified webhooks are an injection vector: anyone who discovers your endpoint URL can trigger your agent with crafted payloads.
Most providers’ signature schemes also include a timestamp to prevent replay attacks. Stripe’s, for example, requires that the timestamp in the signature be within 300 seconds of the current time. Enforce this.
Idempotency: Handle Duplicate Delivery
Webhook providers guarantee at-least-once delivery, not exactly-once. If your endpoint is slow or returns a non-200 status, providers will retry — sometimes multiple times. Your agent will receive the same event twice.
The solution is idempotency at the processing stage. Each webhook event should have a unique ID (GitHub provides X-GitHub-Delivery, Stripe provides event.id). Before processing, check whether you’ve already handled this ID:
def process_webhook_event(event_id: str, payload: dict):
# Check idempotency key
if redis_client.sismember("processed_webhooks", event_id):
return # Already processed, skip
# Process the event
run_agent(payload)
# Mark as processed (with TTL to avoid unbounded growth)
redis_client.sadd("processed_webhooks", event_id)
redis_client.expire("processed_webhooks", 86400 * 7) # 7 days
Handling Out-of-Order Events
Webhooks from the same source can arrive out of order, especially during retries. A PR closed event might arrive before the PR review approved event. Design your agent to handle this:
- Include the event timestamp in your processing logic, not just the arrival time
- Use versioned state — if your agent updates a record based on a webhook, check that the webhook’s timestamp is newer than the record’s last-updated time before overwriting
- For sequential workflows, consider adding a short delay (a few seconds) after the triggering event to allow related events to accumulate before running the agent
Common Webhook Sources for AI Agents
GitHub/GitLab: pull_request, push, issue events. Ideal for code review agents, changelog generators, or CI summary agents that narrate test results in plain English.
Stripe: payment_intent.succeeded, customer.subscription.updated, invoice.payment_failed. Trigger agents that send personalised follow-ups, escalate failed payments, or summarise revenue patterns.
Linear/Jira: Issue created, status changed. Agents that auto-assign issues, draft initial comments, or update related documentation.
Slack/Teams: message, app_mention. Real-time conversational agents triggered only when mentioned, not running constantly.
Email (via Mailgun/Postmark): Inbound email webhooks trigger agents that triage, summarise, draft replies, or route to the right team.
Scaling Considerations
For low-volume pipelines (under a few hundred events/hour), a single Redis list and a worker process are sufficient. At higher volumes:
- Use a proper queue with visibility timeouts and dead-letter queues (SQS, RabbitMQ, Inngest)
- Rate-limit your agent executor to respect LLM API rate limits — don’t let a burst of webhooks trigger 100 simultaneous LLM calls
- Monitor queue depth as your primary SLA metric — if events are sitting in the queue for more than 30 seconds, your executor is falling behind
When Not to Use Webhooks
Webhooks require a publicly reachable endpoint. In local development, use ngrok or cloudflared to expose your local receiver. In production, this means a deployed service — webhooks don’t work if your agent only runs on a laptop.
If you only need to react to events from systems that don’t support webhooks (legacy databases, file systems, internal services), polling remains the right choice. The event-driven pattern is most valuable when the source system supports reliable webhook delivery with signatures and retry logic.
Putting It Together with Low-Code Tools
If you don’t want to build the receiver/queue infrastructure yourself, n8n and Make both support webhook trigger nodes that handle receipt and queueing for you. The webhook URL they provide validates payloads and feeds events directly into your workflow. This is the right starting point for most teams — build the custom infrastructure only when you outgrow what the platforms provide.
Event-triggered agents respond to what’s actually happening, in real time, without the overhead of constant polling. The architecture is straightforward once you’ve handled signatures, idempotency, and the receiver/executor split — and it makes every other agent you build faster and cheaper to run.