Receive webhook events from external services and route them to dedicated agent instances. Each webhook source (repository, customer, device) can have its own agent with isolated state, persistent storage, and real-time client connections.
import { Agent, getAgentByName, routeAgentRequest } from "agents" ;
export class WebhookAgent extends Agent {
async onRequest ( request ) {
if (request.method !== "POST" ) {
return new Response ( "Method not allowed" , { status: 405 });
}
const rawBody = await request. text ();
const signature = request.headers. get ( "X-Hub-Signature-256" );
if (
! ( await verifyGitHubWebhook (rawBody, signature, this .env. WEBHOOK_SECRET ))
) {
return new Response ( "Invalid signature" , { status: 401 });
}
let payload;
try {
payload = JSON . parse (rawBody);
} catch {
return new Response ( "Invalid payload" , { status: 400 });
}
await this . processEvent (payload);
return new Response ( "OK" );
}
async processEvent ( payload ) {
// Store event, update state, trigger actions...
}
}
async function verifyGitHubWebhook ( rawBody , signature , secret ) {
if ( ! signature || ! / ^ sha256= [0-9a-f] {64}$ / i . test (signature)) return false ;
const encoder = new TextEncoder ();
const key = await crypto.subtle. importKey (
"raw" ,
encoder. encode (secret),
{ name: "HMAC" , hash: "SHA-256" },
false ,
[ "verify" ],
);
const signatureBytes = Uint8Array. from (
signature. slice ( "sha256=" . length ). match ( / . {2} / g ) ?? [],
( byte ) => Number. parseInt (byte, 16 ),
);
return crypto.subtle. verify (
"HMAC" ,
key,
signatureBytes,
encoder. encode (rawBody),
);
}
export default {
async fetch ( request , env ) {
const url = new URL (request.url);
if (url.pathname === "/webhooks/github" && request.method === "POST" ) {
const rawBody = await request. clone (). text ();
const signature = request.headers. get ( "X-Hub-Signature-256" );
if (
! ( await verifyGitHubWebhook (rawBody, signature, env. WEBHOOK_SECRET ))
) {
return new Response ( "Invalid signature" , { status: 401 });
}
let payload;
try {
payload = JSON . parse (rawBody);
} catch {
return new Response ( "Invalid payload" , { status: 400 });
}
const repository = payload.repository?.full_name;
if ( ! repository) {
return new Response ( "Missing repository" , { status: 400 });
}
const agentName = repository. toLowerCase (). replace ( / \/ / g , "-" );
const agent = await getAgentByName (env.WebhookAgent, agentName);
return agent. fetch (request);
}
return (
( await routeAgentRequest (request, env)) ||
new Response ( "Not found" , { status: 404 })
);
},
}; import { Agent, getAgentByName, routeAgentRequest } from "agents" ;
export class WebhookAgent extends Agent < Env > {
async onRequest ( request : Request ) : Promise < Response > {
if (request.method !== "POST" ) {
return new Response ( "Method not allowed" , { status: 405 });
}
const rawBody = await request. text ();
const signature = request.headers. get ( "X-Hub-Signature-256" );
if (
! ( await verifyGitHubWebhook (
rawBody,
signature,
this .env. WEBHOOK_SECRET ,
))
) {
return new Response ( "Invalid signature" , { status: 401 });
}
let payload : unknown ;
try {
payload = JSON . parse (rawBody);
} catch {
return new Response ( "Invalid payload" , { status: 400 });
}
await this . processEvent (payload);
return new Response ( "OK" );
}
private async processEvent ( payload : unknown ) {
// Store event, update state, trigger actions...
}
}
async function verifyGitHubWebhook (
rawBody : string ,
signature : string | null ,
secret : string ,
) : Promise < boolean > {
if ( ! signature || ! / ^ sha256= [0-9a-f] {64}$ / i . test (signature)) return false ;
const encoder = new TextEncoder ();
const key = await crypto.subtle. importKey (
"raw" ,
encoder. encode (secret),
{ name: "HMAC" , hash: "SHA-256" },
false ,
[ "verify" ],
);
const signatureBytes = Uint8Array. from (
signature. slice ( "sha256=" . length ). match ( / . {2} / g ) ?? [],
( byte ) => Number. parseInt (byte, 16 ),
);
return crypto.subtle. verify (
"HMAC" ,
key,
signatureBytes,
encoder. encode (rawBody),
);
}
export default {
async fetch ( request : Request , env : Env ) : Promise < Response > {
const url = new URL (request.url);
if (url.pathname === "/webhooks/github" && request.method === "POST" ) {
const rawBody = await request. clone (). text ();
const signature = request.headers. get ( "X-Hub-Signature-256" );
if (
! ( await verifyGitHubWebhook (
rawBody,
signature,
env. WEBHOOK_SECRET ,
))
) {
return new Response ( "Invalid signature" , { status: 401 });
}
let payload : { repository ?: { full_name ?: string } };
try {
payload = JSON . parse (rawBody);
} catch {
return new Response ( "Invalid payload" , { status: 400 });
}
const repository = payload.repository?.full_name;
if ( ! repository) {
return new Response ( "Missing repository" , { status: 400 });
}
const agentName = repository. toLowerCase (). replace ( / \/ / g , "-" );
const agent = await getAgentByName (env.WebhookAgent, agentName);
return agent. fetch (request);
}
return (
( await routeAgentRequest (request, env)) ||
new Response ( "Not found" , { status: 404 })
);
},
} satisfies ExportedHandler < Env >;
Webhooks combined with agents enable patterns where each external entity gets its own isolated, stateful agent instance.
Communication and notifications
Routing webhooks to agents