You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
React to arbitrary HTTP POST events from any system that can deliver a JSON payload — Sentry, Notion, Slack, Drata, PagerDuty, internal services, or anything else. Unlike the GitHub and Linear webhook sources, the generic webhook source has no built-in knowledge of any particular schema; you describe how to extract fields and what to filter on using JSONPath expressions.
275
+
276
+
```yaml
277
+
apiVersion: kelos.dev/v1alpha1
278
+
kind: TaskSpawner
279
+
metadata:
280
+
name: sentry-error-responder
281
+
spec:
282
+
when:
283
+
webhook:
284
+
source: sentry # URL: /webhook/sentry
285
+
fieldMapping:
286
+
id: "$.data.event.event_id" # required — used for deduplication and task naming
287
+
title: "$.data.event.title"
288
+
url: "$.data.url"
289
+
level: "$.data.event.level"
290
+
filters:
291
+
- field: "$.data.event.level"
292
+
value: "error"
293
+
- field: "$.data.event.platform"
294
+
pattern: "^(python|go|node)"
295
+
taskTemplate:
296
+
type: claude-code
297
+
workspaceRef:
298
+
name: my-workspace
299
+
credentials:
300
+
type: oauth
301
+
secretRef:
302
+
name: claude-oauth-token
303
+
promptTemplate: |
304
+
A new Sentry error was reported.
305
+
306
+
Title: {{.Title}}
307
+
Level: {{.level}}
308
+
URL: {{.URL}}
309
+
310
+
Investigate the stack trace in the payload and open a PR with a fix.
311
+
branch: "sentry-{{.ID}}"
312
+
maxConcurrency: 3
313
+
```
314
+
315
+
**Setup:** Enable the `generic` source on `kelos-webhook-server` in your Helm values:
316
+
317
+
```yaml
318
+
# Helm values
319
+
webhookServer:
320
+
sources:
321
+
generic:
322
+
enabled: true
323
+
```
324
+
325
+
The webhook URL is `https://your-webhook-domain/webhook/<source>` (e.g., `/webhook/sentry`).
326
+
327
+
> [!WARNING]
328
+
> **The generic webhook endpoint is currently unauthenticated.** The handler does not validate request signatures, so any client that can reach `/webhook/<source>` and matches a registered TaskSpawner can trigger Task creation. Until per-source HMAC validation is implemented (tracked in [#1040](https://github.com/kelos-dev/kelos/issues/1040)), restrict access at the network layer:
329
+
>
330
+
> - Use a `NetworkPolicy` to limit ingress to known sender CIDRs.
331
+
> - Front the endpoint with an Ingress / Gateway that enforces IP allowlisting or mTLS.
332
+
> - Avoid exposing the webhook Service as `LoadBalancer` on a public network unless ingress is otherwise restricted.
333
+
>
334
+
> The `webhookServer.sources.generic.secretName` Helm value is reserved for future HMAC validation; it currently mounts env vars that no code reads.
335
+
336
+
**Configuration:**
337
+
338
+
- **`source`** *(required)* — short identifier (lowercase alphanumeric with optional hyphens) that determines the URL path (`/webhook/<source>`).
339
+
- **`fieldMapping`** *(required)* — map of template variable name → JSONPath expression evaluated against the request body. Each key becomes `{{.Key}}` in `promptTemplate` and `branch`. Lowercase keys `id`, `title`, `body`, and `url` are also exposed under their canonical uppercase aliases (`{{.ID}}`, `{{.Title}}`, `{{.Body}}`, `{{.URL}}`) for compatibility with templates written for the GitHub or Linear sources. The **`id` key is required** — it is used for delivery deduplication and Task naming. Missing fields produce empty strings (no error); only malformed JSONPath expressions fail.
340
+
- **`filters[]`** *(optional)* — list of conditions that must ALL match for a delivery to trigger a Task (AND semantics across filters). Each filter has a `field` (JSONPath) and exactly one of:
341
+
- `value`— exact string match against the extracted value
342
+
- `pattern`— Go [regexp](https://pkg.go.dev/regexp/syntax) match against the extracted value
343
+
344
+
When `filters` is empty, every delivery triggers a Task. A filter whose `field` is missing in the payload fails (the delivery is skipped).
345
+
346
+
**Generic-webhook variables:** `{{.Kind}}` is always `"GenericWebhook"`, `{{.Payload}}` is the full parsed JSON body (use it for advanced templating like `{{.Payload.data.event.platform}}`), and every key from `fieldMapping` becomes a top-level variable. Standard fields `{{.ID}}`, `{{.Title}}`, `{{.Body}}`, and `{{.URL}}` always exist (empty if not mapped).
347
+
348
+
See [example 13](../examples/13-taskspawner-generic-webhook/) for a full setup walkthrough.
349
+
272
350
### Cron
273
351
274
352
Run agents on a schedule — dependency updates, code health checks, or periodic maintenance.
@@ -300,31 +378,33 @@ spec:
300
378
301
379
All `promptTemplate` and `branch` fields support Go `text/template` syntax. Available variables depend on the source:
| `{{.ID}}` | Issue number (string) | PR number (string) | Issue/PR number or commit ID | Issue key (e.g., `ENG-42`) | Linear resource ID | Date-time string |
306
-
| `{{.Number}}` | Issue number (int) | PR number (int) | Issue/PR number | `0` | Empty | `0` |
307
-
| `{{.Title}}` | Issue title | PR title | Issue/PR title | Issue summary | Resource title | Trigger time (RFC3339) |
308
-
| `{{.Body}}` | Issue body | PR body | Issue/PR/comment body | Issue description | Empty | Empty |
| `{{.ID}}` | Issue number (string) | PR number (string) | Issue/PR number or commit ID | Issue key (e.g., `ENG-42`) | Linear resource ID | Mapped `id` field (required) | Date-time string |
384
+
| `{{.Number}}` | Issue number (int) | PR number (int) | Issue/PR number | `0` | Empty | Empty | `0` |
385
+
| `{{.Title}}` | Issue title | PR title | Issue/PR title | Issue summary | Resource title | Mapped `title` field (if present) | Trigger time (RFC3339) |
386
+
| `{{.Body}}` | Issue body | PR body | Issue/PR/comment body | Issue description | Empty | Mapped `body` field (if present) | Empty |
> **Generic Webhook only:** any additional keys you declare in `fieldMapping` are also exposed as top-level variables. For example, `fieldMapping: {severity: "$.level"}` makes `{{.severity}}` available in templates.
0 commit comments