agents 0.16.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +11 -8
  2. package/dist/{agent-tool-types-CTw3UJUP.d.ts → agent-tool-types-Cd1TZPfB.d.ts} +587 -137
  3. package/dist/agent-tool-types.d.ts +34 -18
  4. package/dist/agent-tool-types.js +20 -1
  5. package/dist/agent-tool-types.js.map +1 -0
  6. package/dist/agent-tools-BXlsuX0d.js +304 -0
  7. package/dist/agent-tools-BXlsuX0d.js.map +1 -0
  8. package/dist/agent-tools-_E8wxUIK.d.ts +119 -0
  9. package/dist/agent-tools.d.ts +24 -18
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/ai-chat-agent.d.ts +1 -1
  12. package/dist/ai-chat-agent.js +1 -2
  13. package/dist/ai-chat-agent.js.map +1 -1
  14. package/dist/ai-chat-v5-migration.d.ts +1 -1
  15. package/dist/ai-chat-v5-migration.js +1 -2
  16. package/dist/ai-chat-v5-migration.js.map +1 -1
  17. package/dist/ai-react.d.ts +1 -1
  18. package/dist/ai-react.js +1 -2
  19. package/dist/ai-react.js.map +1 -1
  20. package/dist/ai-types.d.ts +1 -7
  21. package/dist/ai-types.js +2 -8
  22. package/dist/ai-types.js.map +1 -1
  23. package/dist/browser/ai.js +1 -1
  24. package/dist/browser/index.js +1 -1
  25. package/dist/chat/index.d.ts +1918 -72
  26. package/dist/chat/index.js +1730 -245
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/chat/react.d.ts +602 -0
  29. package/dist/chat/react.js +1506 -0
  30. package/dist/chat/react.js.map +1 -0
  31. package/dist/chat-sdk/index.d.ts +4 -4
  32. package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
  33. package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
  34. package/dist/{client-BXJ9n2f7.js → client-BZ-B3NhC.js} +2 -2
  35. package/dist/client-BZ-B3NhC.js.map +1 -0
  36. package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
  37. package/dist/client.d.ts +76 -57
  38. package/dist/client.js +33 -5
  39. package/dist/client.js.map +1 -1
  40. package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
  41. package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
  42. package/dist/email.js +1 -1
  43. package/dist/email.js.map +1 -1
  44. package/dist/{index-B7IbEeze.d.ts → index-CcbnKkNh.d.ts} +188 -14
  45. package/dist/index.d.ts +91 -71
  46. package/dist/index.js +562 -24
  47. package/dist/index.js.map +1 -1
  48. package/dist/mcp/client.d.ts +18 -14
  49. package/dist/mcp/client.js +1 -1
  50. package/dist/mcp/index.d.ts +30 -30
  51. package/dist/mcp/index.js +7 -7
  52. package/dist/mcp/index.js.map +1 -1
  53. package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
  54. package/dist/message-builder-BymO4N_D.js.map +1 -0
  55. package/dist/observability/index.d.ts +1 -1
  56. package/dist/observability/index.js +4 -2
  57. package/dist/observability/index.js.map +1 -1
  58. package/dist/react.d.ts +123 -110
  59. package/dist/react.js +44 -11
  60. package/dist/react.js.map +1 -1
  61. package/dist/serializable.d.ts +1 -1
  62. package/dist/skills/compile.js +1 -1
  63. package/dist/skills/compile.js.map +1 -1
  64. package/dist/skills/index.js +5 -5
  65. package/dist/skills/index.js.map +1 -1
  66. package/dist/sub-routing.d.ts +6 -6
  67. package/dist/utils.js +1 -1
  68. package/dist/utils.js.map +1 -1
  69. package/dist/vite.js +5 -5
  70. package/dist/vite.js.map +1 -1
  71. package/dist/wire-types-nflOzNuU.js +240 -0
  72. package/dist/wire-types-nflOzNuU.js.map +1 -0
  73. package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
  74. package/dist/workflow-types.d.ts +25 -21
  75. package/dist/workflow-types.js.map +1 -1
  76. package/dist/workflows.d.ts +22 -21
  77. package/dist/workflows.js +31 -7
  78. package/dist/workflows.js.map +1 -1
  79. package/docs/adding-to-existing-project.md +450 -0
  80. package/docs/agent-class.md +503 -0
  81. package/docs/agent-tools.md +552 -0
  82. package/docs/browse-the-web.md +430 -0
  83. package/docs/callable-methods.md +627 -0
  84. package/docs/chat-agents.md +1687 -0
  85. package/docs/chat-sdk.md +181 -0
  86. package/docs/client-sdk.md +520 -0
  87. package/docs/client-tools-continuation.md +177 -0
  88. package/docs/codemode.md +440 -0
  89. package/docs/configuration.md +775 -0
  90. package/docs/cross-domain-authentication.md +171 -0
  91. package/docs/durable-execution.md +537 -0
  92. package/docs/email.md +663 -0
  93. package/docs/get-current-agent.md +204 -0
  94. package/docs/getting-started.md +305 -0
  95. package/docs/http-websockets.md +668 -0
  96. package/docs/human-in-the-loop.md +661 -0
  97. package/docs/index.md +151 -0
  98. package/docs/long-running-agents.md +730 -0
  99. package/docs/mcp-client.md +620 -0
  100. package/docs/mcp-servers.md +526 -0
  101. package/docs/mcp-transports.md +308 -0
  102. package/docs/migration-to-ai-sdk-v5.md +96 -0
  103. package/docs/migration-to-ai-sdk-v6.md +163 -0
  104. package/docs/observability.md +261 -0
  105. package/docs/push-notifications.md +367 -0
  106. package/docs/queue.md +329 -0
  107. package/docs/readonly-connections.md +278 -0
  108. package/docs/resumable-streaming.md +127 -0
  109. package/docs/retries.md +444 -0
  110. package/docs/routing.md +749 -0
  111. package/docs/scheduling.md +898 -0
  112. package/docs/securing-mcp-servers.md +359 -0
  113. package/docs/server-driven-messages.md +477 -0
  114. package/docs/sessions.md +1024 -0
  115. package/docs/state.md +512 -0
  116. package/docs/sub-agents.md +389 -0
  117. package/docs/webhooks.md +604 -0
  118. package/docs/workflows.md +877 -0
  119. package/package.json +41 -14
  120. package/dist/agent-tools-3zLG7MgA.js.map +0 -1
  121. package/dist/agent-tools-DZhI5F6Q.d.ts +0 -14
  122. package/dist/client-BXJ9n2f7.js.map +0 -1
@@ -0,0 +1,604 @@
1
+ # Webhooks
2
+
3
+ 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.
4
+
5
+ ## Quick Start
6
+
7
+ ```typescript
8
+ import { Agent, getAgentByName, routeAgentRequest } from "agents";
9
+
10
+ // Agent that handles webhooks for a specific entity
11
+ export class WebhookAgent extends Agent<Env> {
12
+ async onRequest(request: Request): Promise<Response> {
13
+ if (request.method !== "POST") {
14
+ return new Response("Method not allowed", { status: 405 });
15
+ }
16
+
17
+ // Verify the webhook signature
18
+ const signature = request.headers.get("X-Hub-Signature-256");
19
+ const body = await request.text();
20
+
21
+ if (
22
+ !(await this.verifySignature(body, signature, this.env.WEBHOOK_SECRET))
23
+ ) {
24
+ return new Response("Invalid signature", { status: 401 });
25
+ }
26
+
27
+ // Process the webhook payload
28
+ const payload = JSON.parse(body);
29
+ await this.processEvent(payload);
30
+
31
+ return new Response("OK", { status: 200 });
32
+ }
33
+
34
+ private async verifySignature(
35
+ payload: string,
36
+ signature: string | null,
37
+ secret: string
38
+ ): Promise<boolean> {
39
+ if (!signature) return false;
40
+
41
+ const encoder = new TextEncoder();
42
+ const key = await crypto.subtle.importKey(
43
+ "raw",
44
+ encoder.encode(secret),
45
+ { name: "HMAC", hash: "SHA-256" },
46
+ false,
47
+ ["sign"]
48
+ );
49
+
50
+ const signatureBytes = await crypto.subtle.sign(
51
+ "HMAC",
52
+ key,
53
+ encoder.encode(payload)
54
+ );
55
+ const expected = `sha256=${Array.from(new Uint8Array(signatureBytes))
56
+ .map((b) => b.toString(16).padStart(2, "0"))
57
+ .join("")}`;
58
+
59
+ return signature === expected;
60
+ }
61
+
62
+ private async processEvent(payload: unknown) {
63
+ // Store event, update state, trigger actions...
64
+ }
65
+ }
66
+
67
+ // Route webhooks to the right agent instance
68
+ export default {
69
+ async fetch(request: Request, env: Env): Promise<Response> {
70
+ const url = new URL(request.url);
71
+
72
+ // Webhook endpoint: POST /webhooks/:entityId
73
+ if (url.pathname.startsWith("/webhooks/") && request.method === "POST") {
74
+ const entityId = url.pathname.split("/")[2];
75
+ const agent = await getAgentByName(env.WebhookAgent, entityId);
76
+ return agent.fetch(request);
77
+ }
78
+
79
+ // Default routing for WebSocket connections
80
+ return (
81
+ (await routeAgentRequest(request, env)) ||
82
+ new Response("Not found", { status: 404 })
83
+ );
84
+ }
85
+ };
86
+ ```
87
+
88
+ ## Use Cases
89
+
90
+ Webhooks combined with agents enable powerful patterns where each external entity gets its own isolated, stateful agent instance.
91
+
92
+ ### Developer Tools
93
+
94
+ | Use Case | Description |
95
+ | ------------------------ | -------------------------------------------------------------------------- |
96
+ | **GitHub Repo Monitor** | One agent per repository tracking commits, PRs, issues, and stars |
97
+ | **CI/CD Pipeline Agent** | React to build/deploy events, notify on failures, track deployment history |
98
+ | **Linear/Jira Tracker** | Auto-triage issues, assign based on content, track resolution times |
99
+
100
+ ### E-commerce & Payments
101
+
102
+ | Use Case | Description |
103
+ | -------------------------- | --------------------------------------------------------------------- |
104
+ | **Stripe Customer Agent** | One agent per customer tracking payments, subscriptions, and disputes |
105
+ | **Shopify Order Agent** | Order lifecycle from creation to fulfillment with inventory sync |
106
+ | **Payment Reconciliation** | Match webhook events to internal records, flag discrepancies |
107
+
108
+ ### Communication & Notifications
109
+
110
+ | Use Case | Description |
111
+ | -------------------- | ----------------------------------------------------------------------- |
112
+ | **Twilio SMS/Voice** | Conversational agents triggered by inbound messages or calls |
113
+ | **Slack Bot** | Respond to slash commands, button clicks, and interactive messages |
114
+ | **Email Tracking** | SendGrid/Mailgun delivery events, bounce handling, engagement analytics |
115
+
116
+ ### IoT & Infrastructure
117
+
118
+ | Use Case | Description |
119
+ | --------------------- | ------------------------------------------------------------ |
120
+ | **Device Telemetry** | One agent per device processing sensor data streams |
121
+ | **Alert Aggregation** | Collect alerts from PagerDuty, Datadog, or custom monitoring |
122
+ | **Home Automation** | React to IFTTT/Zapier triggers with persistent state |
123
+
124
+ ### SaaS Integrations
125
+
126
+ | Use Case | Description |
127
+ | -------------------- | --------------------------------------------------------------- |
128
+ | **CRM Sync** | Salesforce/HubSpot contact and deal updates |
129
+ | **Calendar Agent** | Google Calendar event notifications and scheduling |
130
+ | **Form Submissions** | Typeform, Tally, or custom form webhooks with follow-up actions |
131
+
132
+ ## Routing Webhooks to Agents
133
+
134
+ The key pattern is extracting an entity identifier from the webhook and using `getAgentByName()` to route to a dedicated agent instance.
135
+
136
+ ### Extract Entity from Payload
137
+
138
+ Most webhooks include an identifier in the payload:
139
+
140
+ ```typescript
141
+ export default {
142
+ async fetch(request: Request, env: Env): Promise<Response> {
143
+ if (request.method === "POST" && url.pathname === "/webhooks/github") {
144
+ const payload = await request.clone().json();
145
+
146
+ // Extract entity ID from payload
147
+ const repoFullName = payload.repository?.full_name;
148
+ if (!repoFullName) {
149
+ return new Response("Missing repository", { status: 400 });
150
+ }
151
+
152
+ // Sanitize for use as agent name
153
+ const agentName = repoFullName.toLowerCase().replace(/\//g, "-");
154
+
155
+ // Route to dedicated agent
156
+ const agent = await getAgentByName(env.RepoAgent, agentName);
157
+ return agent.fetch(request);
158
+ }
159
+ }
160
+ };
161
+ ```
162
+
163
+ ### Extract Entity from URL
164
+
165
+ Alternatively, include the entity ID in the webhook URL:
166
+
167
+ ```typescript
168
+ // Webhook URL: https://your-worker.dev/webhooks/stripe/cus_123456
169
+ if (url.pathname.startsWith("/webhooks/stripe/")) {
170
+ const customerId = url.pathname.split("/")[3]; // "cus_123456"
171
+ const agent = await getAgentByName(env.StripeAgent, customerId);
172
+ return agent.fetch(request);
173
+ }
174
+ ```
175
+
176
+ ### Extract Entity from Headers
177
+
178
+ Some services include identifiers in headers:
179
+
180
+ ```typescript
181
+ // Slack sends workspace info in headers
182
+ const teamId = request.headers.get("X-Slack-Team-Id");
183
+ if (teamId) {
184
+ const agent = await getAgentByName(env.SlackAgent, teamId);
185
+ return agent.fetch(request);
186
+ }
187
+ ```
188
+
189
+ ## Signature Verification
190
+
191
+ Always verify webhook signatures to ensure requests are authentic. Most providers use HMAC-SHA256.
192
+
193
+ ### HMAC-SHA256 Pattern
194
+
195
+ ```typescript
196
+ async function verifySignature(
197
+ payload: string,
198
+ signature: string | null,
199
+ secret: string
200
+ ): Promise<boolean> {
201
+ if (!signature) return false;
202
+
203
+ const encoder = new TextEncoder();
204
+ const key = await crypto.subtle.importKey(
205
+ "raw",
206
+ encoder.encode(secret),
207
+ { name: "HMAC", hash: "SHA-256" },
208
+ false,
209
+ ["sign"]
210
+ );
211
+
212
+ const signatureBytes = await crypto.subtle.sign(
213
+ "HMAC",
214
+ key,
215
+ encoder.encode(payload)
216
+ );
217
+
218
+ const expected = `sha256=${Array.from(new Uint8Array(signatureBytes))
219
+ .map((b) => b.toString(16).padStart(2, "0"))
220
+ .join("")}`;
221
+
222
+ // Use timing-safe comparison in production
223
+ return signature === expected;
224
+ }
225
+ ```
226
+
227
+ ### Provider-Specific Headers
228
+
229
+ | Provider | Signature Header | Algorithm |
230
+ | -------- | ----------------------- | ---------------------------- |
231
+ | GitHub | `X-Hub-Signature-256` | HMAC-SHA256 |
232
+ | Stripe | `Stripe-Signature` | HMAC-SHA256 (with timestamp) |
233
+ | Twilio | `X-Twilio-Signature` | HMAC-SHA1 |
234
+ | Slack | `X-Slack-Signature` | HMAC-SHA256 (with timestamp) |
235
+ | Shopify | `X-Shopify-Hmac-Sha256` | HMAC-SHA256 (base64) |
236
+
237
+ ## Processing Webhooks
238
+
239
+ ### The onRequest Handler
240
+
241
+ Use `onRequest()` to handle incoming webhooks in your agent:
242
+
243
+ ```typescript
244
+ export class WebhookAgent extends Agent<Env, MyState> {
245
+ async onRequest(request: Request): Promise<Response> {
246
+ // 1. Validate method
247
+ if (request.method !== "POST") {
248
+ return new Response("Method not allowed", { status: 405 });
249
+ }
250
+
251
+ // 2. Get event type from headers
252
+ const eventType = request.headers.get("X-Event-Type");
253
+
254
+ // 3. Verify signature
255
+ const signature = request.headers.get("X-Signature");
256
+ const body = await request.text();
257
+
258
+ if (!(await this.verifySignature(body, signature))) {
259
+ return new Response("Invalid signature", { status: 401 });
260
+ }
261
+
262
+ // 4. Parse and process
263
+ const payload = JSON.parse(body);
264
+ await this.handleEvent(eventType, payload);
265
+
266
+ // 5. Respond quickly
267
+ return new Response("OK", { status: 200 });
268
+ }
269
+
270
+ private async handleEvent(type: string, payload: unknown) {
271
+ // Update state (broadcasts to connected clients)
272
+ this.setState({
273
+ ...this.state,
274
+ lastEventType: type,
275
+ lastEventTime: new Date().toISOString()
276
+ });
277
+
278
+ // Store in SQL for history
279
+ this
280
+ .sql`INSERT INTO events (type, payload, timestamp) VALUES (${type}, ${JSON.stringify(payload)}, ${Date.now()})`;
281
+ }
282
+ }
283
+ ```
284
+
285
+ ## Storing Webhook Events
286
+
287
+ Use SQLite to persist webhook events for history and replay.
288
+
289
+ ### Event Table Schema
290
+
291
+ ```typescript
292
+ async onStart(): Promise<void> {
293
+ this.sql`
294
+ CREATE TABLE IF NOT EXISTS events (
295
+ id TEXT PRIMARY KEY,
296
+ type TEXT NOT NULL,
297
+ action TEXT,
298
+ title TEXT NOT NULL,
299
+ description TEXT,
300
+ url TEXT,
301
+ actor TEXT,
302
+ payload TEXT,
303
+ timestamp TEXT NOT NULL
304
+ )
305
+ `;
306
+
307
+ this.sql`
308
+ CREATE INDEX IF NOT EXISTS idx_events_timestamp
309
+ ON events(timestamp DESC)
310
+ `;
311
+ }
312
+ ```
313
+
314
+ ### Cleanup Old Events
315
+
316
+ Prevent unbounded growth by keeping only recent events:
317
+
318
+ ```typescript
319
+ // Keep last 100 events
320
+ this.sql`
321
+ DELETE FROM events WHERE id NOT IN (
322
+ SELECT id FROM events ORDER BY timestamp DESC LIMIT 100
323
+ )
324
+ `;
325
+
326
+ // Or delete events older than 30 days
327
+ this.sql`
328
+ DELETE FROM events
329
+ WHERE timestamp < datetime('now', '-30 days')
330
+ `;
331
+ ```
332
+
333
+ ### Query Events
334
+
335
+ ```typescript
336
+ @callable()
337
+ getEvents(limit = 20) {
338
+ return [...this.sql`
339
+ SELECT * FROM events
340
+ ORDER BY timestamp DESC
341
+ LIMIT ${limit}
342
+ `];
343
+ }
344
+
345
+ @callable()
346
+ getEventsByType(type: string, limit = 20) {
347
+ return [...this.sql`
348
+ SELECT * FROM events
349
+ WHERE type = ${type}
350
+ ORDER BY timestamp DESC
351
+ LIMIT ${limit}
352
+ `];
353
+ }
354
+ ```
355
+
356
+ ## Real-time Broadcasting
357
+
358
+ When a webhook arrives, update agent state to automatically broadcast to connected WebSocket clients.
359
+
360
+ ```typescript
361
+ // In your agent
362
+ private async processWebhook(eventType: string, payload: WebhookPayload) {
363
+ // Update state - this automatically broadcasts to all connected clients
364
+ this.setState({
365
+ ...this.state,
366
+ stats: payload.stats,
367
+ lastEvent: {
368
+ type: eventType,
369
+ timestamp: new Date().toISOString()
370
+ }
371
+ });
372
+ }
373
+ ```
374
+
375
+ On the client side:
376
+
377
+ ```tsx
378
+ import { useAgent } from "agents/react";
379
+
380
+ function Dashboard() {
381
+ const agent = useAgent({
382
+ agent: "webhook-agent",
383
+ name: "my-entity-id"
384
+ });
385
+
386
+ return <div>Last event: {agent.state?.lastEvent?.type}</div>;
387
+ }
388
+ ```
389
+
390
+ ## Patterns
391
+
392
+ ### Event Deduplication
393
+
394
+ Prevent processing duplicate events using event IDs:
395
+
396
+ ```typescript
397
+ async handleEvent(eventId: string, payload: unknown) {
398
+ // Check if already processed
399
+ const existing = [...this.sql`
400
+ SELECT id FROM events WHERE id = ${eventId}
401
+ `];
402
+
403
+ if (existing.length > 0) {
404
+ console.log(`Event ${eventId} already processed, skipping`);
405
+ return;
406
+ }
407
+
408
+ // Process and store
409
+ await this.processPayload(payload);
410
+ this.sql`INSERT INTO events (id, ...) VALUES (${eventId}, ...)`;
411
+ }
412
+ ```
413
+
414
+ ### Respond Quickly, Process Asynchronously
415
+
416
+ Webhook providers expect fast responses. Use the queue for heavy processing:
417
+
418
+ ```typescript
419
+ async onRequest(request: Request): Promise<Response> {
420
+ const payload = await request.json();
421
+
422
+ // Quick validation
423
+ if (!this.isValid(payload)) {
424
+ return new Response("Invalid", { status: 400 });
425
+ }
426
+
427
+ // Queue heavy processing
428
+ await this.queue("processWebhook", payload);
429
+
430
+ // Respond immediately
431
+ return new Response("Accepted", { status: 202 });
432
+ }
433
+
434
+ async processWebhook(payload: WebhookPayload) {
435
+ // Heavy processing happens here, after response sent
436
+ await this.enrichData(payload);
437
+ await this.notifyDownstream(payload);
438
+ await this.updateAnalytics(payload);
439
+ }
440
+ ```
441
+
442
+ If the asynchronous work is a single Think chat turn, use
443
+ `submitMessages()` instead. It returns a durable submission ID immediately and
444
+ lets retries use an idempotency key instead of duplicating the message turn:
445
+
446
+ ```typescript
447
+ const submission = await this.submitMessages(messages, {
448
+ idempotencyKey: payload.id
449
+ });
450
+
451
+ return Response.json(
452
+ { submissionId: submission.submissionId },
453
+ { status: 202 }
454
+ );
455
+ ```
456
+
457
+ If the webhook owns application side effects around a turn, such as restoring a
458
+ provider thread and posting a visible reply, use
459
+ [`startFiber()`](./durable-execution.md#startfiber) around that job. Managed
460
+ fibers retain status, dedupe provider retries, and let `onFiberRecovered()` or
461
+ `resolveFiber()` record the app-level recovery outcome.
462
+
463
+ ### Multi-Provider Routing
464
+
465
+ Handle webhooks from multiple services in one worker:
466
+
467
+ ```typescript
468
+ export default {
469
+ async fetch(request: Request, env: Env): Promise<Response> {
470
+ const url = new URL(request.url);
471
+
472
+ if (request.method === "POST") {
473
+ // GitHub webhooks
474
+ if (url.pathname.startsWith("/webhooks/github/")) {
475
+ const payload = await request.clone().json();
476
+ const repoName = payload.repository?.full_name?.replace("/", "-");
477
+ const agent = await getAgentByName(env.GitHubAgent, repoName);
478
+ return agent.fetch(request);
479
+ }
480
+
481
+ // Stripe webhooks
482
+ if (url.pathname.startsWith("/webhooks/stripe/")) {
483
+ const payload = await request.clone().json();
484
+ const customerId = payload.data?.object?.customer;
485
+ const agent = await getAgentByName(env.StripeAgent, customerId);
486
+ return agent.fetch(request);
487
+ }
488
+
489
+ // Slack webhooks
490
+ if (url.pathname === "/webhooks/slack") {
491
+ const teamId = request.headers.get("X-Slack-Team-Id");
492
+ const agent = await getAgentByName(env.SlackAgent, teamId);
493
+ return agent.fetch(request);
494
+ }
495
+ }
496
+
497
+ return (
498
+ (await routeAgentRequest(request, env)) ??
499
+ new Response("Not found", { status: 404 })
500
+ );
501
+ }
502
+ };
503
+ ```
504
+
505
+ ## Sending Outgoing Webhooks
506
+
507
+ Agents can also send webhooks to external services:
508
+
509
+ ```typescript
510
+ export class NotificationAgent extends Agent<Env> {
511
+ async notifySlack(message: string) {
512
+ const response = await fetch(this.env.SLACK_WEBHOOK_URL, {
513
+ method: "POST",
514
+ headers: { "Content-Type": "application/json" },
515
+ body: JSON.stringify({ text: message })
516
+ });
517
+
518
+ if (!response.ok) {
519
+ throw new Error(`Slack notification failed: ${response.status}`);
520
+ }
521
+ }
522
+
523
+ async sendSignedWebhook(url: string, payload: unknown) {
524
+ const body = JSON.stringify(payload);
525
+ const signature = await this.sign(body, this.env.WEBHOOK_SECRET);
526
+
527
+ await fetch(url, {
528
+ method: "POST",
529
+ headers: {
530
+ "Content-Type": "application/json",
531
+ "X-Signature": signature
532
+ },
533
+ body
534
+ });
535
+ }
536
+ }
537
+ ```
538
+
539
+ ## Security Best Practices
540
+
541
+ 1. **Always verify signatures** - Never trust unverified webhooks
542
+ 2. **Use environment secrets** - Store secrets with `wrangler secret put`, not in code
543
+ 3. **Respond quickly** - Return 200/202 within seconds to avoid retries
544
+ 4. **Validate payloads** - Check required fields before processing
545
+ 5. **Log rejections** - Track invalid signatures for security monitoring
546
+ 6. **Use HTTPS** - Webhook URLs should always use TLS
547
+
548
+ ```typescript
549
+ // Store secrets securely
550
+ // wrangler secret put GITHUB_WEBHOOK_SECRET
551
+
552
+ // Access in agent
553
+ const secret = this.env.GITHUB_WEBHOOK_SECRET;
554
+ ```
555
+
556
+ ## Complete Example
557
+
558
+ See the [GitHub Webhook Dashboard](https://github.com/cloudflare/agents/tree/main/examples/github-webhook) for a full implementation featuring:
559
+
560
+ - HMAC-SHA256 signature verification
561
+ - Agent-per-repository routing
562
+ - SQLite event storage
563
+ - Real-time WebSocket broadcasting
564
+ - React dashboard with live updates
565
+
566
+ ### Architecture
567
+
568
+ ```
569
+ GitHub Cloudflare Worker Client
570
+ │ │ │
571
+ │ POST /webhooks/github │ │
572
+ │ X-Hub-Signature-256: sha256=... │ │
573
+ │ {"repository": {"full_name": ...}}│ │
574
+ │ ──────────────────────────────────>│ │
575
+ │ │ │
576
+ │ ┌───────┴───────┐ │
577
+ │ │ Verify sig │ │
578
+ │ │ Extract repo │ │
579
+ │ │ getAgentByName│ │
580
+ │ └───────┬───────┘ │
581
+ │ │ │
582
+ │ ┌───────┴───────┐ │
583
+ │ │ RepoAgent │ │
584
+ │ │ (per repo) │ │
585
+ │ │ │ │
586
+ │ │ • Update state│──── WebSocket ───────>│
587
+ │ │ • Store event │ │
588
+ │ │ • Broadcast │ │
589
+ │ └───────────────┘ │
590
+ │ │ │
591
+ │<─────────── 200 OK ────────────────│ │
592
+ ```
593
+
594
+ ## Common Webhook Providers
595
+
596
+ | Provider | Documentation |
597
+ | -------- | ------------------------------------------------------------------------------------------------------------ |
598
+ | GitHub | [Webhook events and payloads](https://docs.github.com/en/webhooks) |
599
+ | Stripe | [Webhook signatures](https://stripe.com/docs/webhooks/signatures) |
600
+ | Twilio | [Validate webhook requests](https://www.twilio.com/docs/usage/webhooks/webhooks-security) |
601
+ | Slack | [Verifying requests](https://api.slack.com/authentication/verifying-requests-from-slack) |
602
+ | Shopify | [Webhook verification](https://shopify.dev/docs/apps/webhooks/configuration/https#step-5-verify-the-webhook) |
603
+ | SendGrid | [Event webhook](https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook) |
604
+ | Linear | [Webhooks](https://developers.linear.app/docs/graphql/webhooks) |