Back to Lab
RAXXO Studios 8 min read No time? Make it a 1 min read

Claude Webhooks: 4 Production Patterns for Agent-Driven External Alerts

Agents
8 min read
TLDR
×
  • Claude Webhooks shipped public beta on May 6 2026 for Managed Agents, sending HTTP POSTs when an agent decides to ping you
  • Pattern 1 routes long-task completion to a Slack incoming webhook with HMAC signature verification
  • Pattern 2 auto-creates Linear tickets when an agent flags a blocker, deduped via idempotency key
  • Pattern 3 patches GitHub issue comments after PR review agents finish, using the issue number from the agent context
  • Pattern 4 forwards SLO and cost events to a custom monitoring endpoint, treating delivery as best-effort with at-least-once semantics

I have been wiring webhooks into Claude Managed Agents for a week now and I am in that awkward phase where the feature works, the docs are thin, and half the production patterns you actually need are not in the announcement post. So this is the missing manual.

Anthropic shipped Webhooks for Managed Agents into public beta on May 6 at the SF dev conference, alongside Dreaming, Result Loops, and Multi-Agent Orchestration. Useful framing: webhooks are the only one of the four that talks to systems outside Claude. Everything else stays inside the agent loop. Webhooks are how the agent tells the rest of your stack that something happened.

Here are the four patterns I have actually run in production, with the payload shapes and the gotchas I would have wanted on day one.

Pattern 1: Slack ping when a long task completes

The simplest pattern, and the one most people will start with. You have an agent doing a 40-minute job (data crunch, batch refactor, scrape with retries). You want a Slack message in #ops the moment it finishes, success or fail.

Configure the webhook on the agent with `event: "task.completed"` and a target URL that points at a Slack incoming webhook. Anthropic signs every request with HMAC-SHA256, so verify before you trust the payload.


import crypto from "node:crypto";

export async function POST(req: Request) {
  const raw = await req.text();
  const sig = req.headers.get("x-anthropic-signature") ?? "";
  const ts  = req.headers.get("x-anthropic-timestamp") ?? "";

  const expected = crypto
    .createHmac("sha256", process.env.ANTHROPIC_SIGNING_KEY!)
    .update(`${ts}.${raw}`)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return new Response("bad signature", { status: 401 });
  }

  const body = JSON.parse(raw);
  await fetch(process.env.SLACK_WEBHOOK_URL!, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      text: `Agent ${body.agent_id} finished task ${body.task_id} in ${body.duration_ms}ms`,
    }),
  });

  return new Response("ok", { status: 200 });
}

The payload Anthropic sends looks roughly like this:


{
  "event": "task.completed",
  "delivery_id": "wh_01HXY...",
  "agent_id": "agt_abc123",
  "task_id": "tsk_def456",
  "status": "succeeded",
  "duration_ms": 2417000,
  "result_summary": "...",
  "ts": "2026-05-16T10:42:09Z"
}

One thing the docs gloss over: the `delivery_id` is your friend. Anthropic retries failed deliveries with exponential backoff up to 24 hours, and the same `delivery_id` is reused across retries. So your Slack handler should dedupe on it, otherwise your channel gets four pings for one finished task at 3am.

Pattern 2: Auto-create a Linear ticket when an agent hits a blocker

This is the pattern that bought me back about two hours a week. Agents working on long jobs sometimes hit a state where they cannot proceed, missing credentials, ambiguous requirements, a tool returning a permanent 403. Instead of making the agent guess or stall, I let it fire a `task.blocked` webhook and turn that into a Linear ticket.


const body = await verifyAndParse(req);
if (body.event !== "task.blocked") return new Response("skip", { status: 200 });

// Idempotency: the agent might fire blocked twice if it retries internally
const idemKey = body.delivery_id;

const linearRes = await fetch("https://api.linear.app/graphql", {
  method: "POST",
  headers: {
    "authorization": process.env.LINEAR_API_KEY!,
    "content-type": "application/json",
    "idempotency-key": idemKey,
  },
  body: JSON.stringify({
    query: `
      mutation IssueCreate($input: IssueCreateInput!) {
        issueCreate(input: $input) { success issue { id identifier url } }
      }`,
    variables: {
      input: {
        teamId: process.env.LINEAR_TEAM_ID,
        title: `[Agent blocked] ${body.task_summary}`,
        description: [
          `Agent: ${body.agent_id}`,
          `Task: ${body.task_id}`,
          `Reason: ${body.blocker_reason}`,
          `Last action: ${body.last_action}`,
          ``,
          `Trace: https://console.anthropic.com/agents/${body.agent_id}/runs/${body.run_id}`,
        ].join("\n"),
        labelIds: [process.env.LINEAR_LABEL_AGENT_BLOCKED],
        priority: 2,
      },
    },
  }),
});

Two things I learned the hard way. First, Linear's GraphQL API does not honor `idempotency-key` natively, so if you want to be safe you store seen `delivery_id` values in a small KV (Upstash, Redis, even a Postgres table) before calling Linear. Second, the agent's `task_summary` field is short, but `last_action` can be long enough to tip Linear's description over its limit. Truncate to 8000 chars to be safe.

If you want the broader context on how Managed Agents tee up these blocker signals in the first place, the Multi-Agent Orchestration walkthrough covers how the orchestrator and specialists negotiate handoffs.

Pattern 3: Update a GitHub issue when a PR review agent finishes

This one needs the agent context to carry the GitHub issue number through the review. I pass it as `metadata.github_issue` when I create the agent run, and Claude echoes it back in the webhook payload.

The flow: I open a PR, label it `agent-review`, a webhook on my side spins up a Claude review agent, the agent posts findings as a comment, fires `review.completed`, and my handler closes the loop by updating the linked issue.


const body = await verifyAndParse(req);
if (body.event !== "review.completed") return new Response("skip", { status: 200 });

const issueNumber = body.metadata?.github_issue;
if (!issueNumber) return new Response("no issue", { status: 200 });

const verdict = body.result.verdict; // "approve" | "request_changes" | "comment"
const body_md = [
  `Claude review agent finished.`,
  ``,
  `Verdict: ${verdict}`,
  `Findings: ${body.result.findings.length}`,
  `Critical: ${body.result.findings.filter(f => f.severity === "critical").length}`,
  ``,
  `Full report: ${body.result.report_url}`,
].join("\n");

await fetch(`https://api.github.com/repos/${process.env.GH_OWNER}/${process.env.GH_REPO}/issues/${issueNumber}/comments`, {
  method: "POST",
  headers: {
    "authorization": `Bearer ${process.env.GH_TOKEN}`,
    "accept": "application/vnd.github+json",
  },
  body: JSON.stringify({ body: body_md }),
});

A subtle thing: GitHub's REST API does not have idempotency, so retried webhook deliveries will create duplicate comments. I keep a small SQLite file mapping `delivery_id -> github_comment_id`, and on retry I look up the existing comment and PATCH it instead of POSTing a new one. Five lines of logic, saves a lot of inbox noise.

For the wider review-loop story, the Result Loops piece goes through how I turn agent verdicts into rubric-graded scores before I trust them on a PR.

Pattern 4: Custom SLO and cost monitor

The fourth pattern is the one that pays for itself fastest, especially if you are running a lot of agents in parallel. Anthropic exposes per-event cost and latency in the webhook payload, so you can fan it out to your own monitoring endpoint and build whatever dashboards you want without touching the console.


const body = await verifyAndParse(req);

await fetch(process.env.METRICS_INGEST_URL!, {
  method: "POST",
  headers: { "content-type": "application/json", "x-api-key": process.env.METRICS_KEY! },
  body: JSON.stringify({
    series: [
      { name: "agent.duration_ms", value: body.duration_ms, tags: { agent: body.agent_id, status: body.status } },
      { name: "agent.cost_eur",  value: body.cost_eur,    tags: { agent: body.agent_id, model: body.model } },
      { name: "agent.tokens.in",  value: body.tokens.input,  tags: { agent: body.agent_id } },
      { name: "agent.tokens.out", value: body.tokens.output, tags: { agent: body.agent_id } },
    ],
    ts: body.ts,
    delivery_id: body.delivery_id,
  }),
});

Two honesty notes here.

Webhook delivery is best-effort with at-least-once semantics. Anthropic retries up to 24 hours but eventually gives up. If your monitoring story depends on hitting 100% of events, you cannot rely on webhooks alone, you need a periodic pull from the agents API as a backstop. I pull every 15 minutes and reconcile.

The cost numbers in the webhook payload are estimates at fire time. They tend to settle to the final invoice number within an hour, but I have seen a 3 to 7 percent drift on long runs. For internal dashboards that is fine. For client billing, wait for the daily usage export.

If you want the broader take on how agent costs accumulate when you run them in parallel, the Multi-Agent Orchestration piece breaks down where the spend actually goes.

Bottom line

Claude Webhooks are the simplest of the four May 6 announcements, and probably the most useful for anyone who already has an agent in production. The patterns are not exotic, they are the same Stripe-style or GitHub-style webhook patterns you have seen for years, with HMAC signing and at-least-once delivery.

The thing to internalize: an agent is now a system that talks back to your stack, not a black box you poll. Treat the webhook endpoint like any other production receiver. Verify the signature, dedupe on `delivery_id`, store enough state to handle retries, and have a periodic pull as a backstop for the events you cannot afford to miss.

If you are building production workflows on top of Managed Agents, the Claude Blueprint bundles the prompt patterns, hooks, and skill scaffolding I use day to day, including the webhook receiver template above.

Stay in the loop
New tools, drops, and AI experiments. No spam. Unsubscribe anytime.
Back to all articles