Skip to content

feat(webhooks): scope event subscriptions to specific agents/workflows#216

Merged
JohnD-EE merged 3 commits into
mainfrom
feat/scoped-event-subscriptions
May 23, 2026
Merged

feat(webhooks): scope event subscriptions to specific agents/workflows#216
JohnD-EE merged 3 commits into
mainfrom
feat/scoped-event-subscriptions

Conversation

@JohnD-EE
Copy link
Copy Markdown
Contributor

Summary

Adds optional agent and workflow filter arrays to AiWebhookSubscription so admins can scope a subscription to specific entities instead of receiving every event of a given type. Empty arrays preserve current behaviour — existing rows continue delivering as before.

Why

Operators running multiple agents/workflows had only one knob: subscribe to an event type and get it for every entity. That meant either flooding receivers, splitting one logical alert across N near-identical subscriptions, or filtering downstream. This PR closes the gap with two small filter columns and a centralised matching rule.

How it works

Filtering is dimension-specific: an agentIds filter only restricts events that carry an agent ID; a workflowIds filter only restricts workflow-typed events. Setting an agent filter does NOT silence unrelated workflow events. The map at lib/orchestration/webhooks/event-entity-keys.ts is the single source of truth for which payload field carries each entity ID; a coverage test asserts every wired event type is mapped.

Fails closed when a mapped event's payload is missing the expected ID — a scoped sub with a non-empty filter on that dimension does NOT match. Prevents an upstream enrichment bug from leaking events to scoped subscribers.

What landed

  • Schema: two String[] @default([]) columns on AiWebhookSubscription + additive migration (add_entity_scope_to_webhook_subscription).
  • Dispatcher: lib/orchestration/webhooks/dispatcher.ts runs matchesEntityScope(...) over the result of findMany before scheduling deliveries.
  • Payload enrichment: workflow_budget_exceeded now carries workflowId from all four engine call sites, so workflow-typed subs can scope it.
  • Validation + API: optional agentIds/workflowIds flow through createWebhookSchema / updateWebhookSchema and the route handlers; 50-entry cap per dimension.
  • Admin form: new "Scope" block between Events and Retry policy, two async-search MultiSelects (agents + workflows). Workflow picker filters templates server-side (isTemplate=false) since templates are never event subjects. <FieldHelp> copy explains the dimension-specific rule.
  • List table: small Scoped badge with count tooltip when either filter array is non-empty.
  • Docs: "Entity-Scoped Subscriptions" section in .context/orchestration/hooks.md; form + table updates in .context/admin/orchestration-webhooks.md.

Tests

  • 7 dispatcher tests for the dimension-specific rule (empty filters, agent match, agent miss, workflow-typed event with agent-only filter, etc.) + fail-closed and routing-to-only-matching-sub coverage.
  • 10 unit tests for event-entity-keys.ts including a coverage assertion that every entry in WEBHOOK_EVENT_TYPES has a deliberate map entry.
  • 4 form tests (Scope section renders, edit-mode prefetch resolves chip labels, empty-scope skips prefetch, workflow loader sets isTemplate=false).
  • 3 table tests (Scoped badge with agents, with workflows, hidden when both empty).
  • 1 engine test asserting workflow_budget_exceeded carries workflowId end-to-end.

All 4011 tests pass. Validate clean (0 errors). webhook-form.tsx coverage went from 70.6/77.6/73.8/68.2 → 91.9/86.7/85.7/89.2 (all ≥80%).

Test plan

  • Pull, run npm run db:migrate:dev — non-destructive additive migration applies.
  • Create Sub A: events=[`budget_exceeded`,`workflow_failed`], `agentIds=[agent-X]`, no workflow filter. Sub B: same events, no filters.
  • Trigger `budget_exceeded` for agent-X (chat to budget=0). Both A and B get deliveries.
  • Trigger `budget_exceeded` for agent-Y. Only B gets a delivery.
  • Fail any workflow. Both A and B fire (agent filter doesn't apply to workflow events).
  • Add `workflowIds=[workflow-Z]` to A. Fail workflow-Z → both fire. Fail workflow-Q → only B.
  • Confirm "Limit to workflows" picker shows no templates.
  • Confirm "Scoped" badge appears next to any sub that has filters set.

Backward compatibility

Fully backward compatible. Existing subscription rows default to empty arrays, which the matcher treats as "no constraint". The schema migration is purely additive. No API contract changes for existing fields.

Commits

  • `e50259a6` — feat(webhooks): scope event subscriptions to specific agents/workflows
  • `fa3bbc98` — feat(webhooks): exclude templates from workflow scope picker
  • `52a8c957` — test(webhooks): cover entity-scope form + Scoped badge

🤖 Generated with Claude Code

JohnD-EE and others added 3 commits May 23, 2026 20:32
Adds optional agentIds and workflowIds filter arrays to AiWebhookSubscription
so admins can limit a subscription to specific entities instead of receiving
every event of a given type. Empty arrays preserve current behaviour
(no constraint), so existing rows keep delivering as before.

Filtering is dimension-specific: an agentIds filter only restricts
events that carry an agent ID; a workflowIds filter only restricts
workflow-typed events. A new EVENT_ENTITY_KEYS map is the single source
of truth for which payload field carries each entity ID. Fails closed
when the payload is missing the expected ID — prevents enrichment bugs
from leaking events to scoped subscribers.

Also enriches workflow_budget_exceeded with workflowId so engine call
sites can target it via the workflow filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pass `isTemplate=false` to the workflows list endpoint when loading
options for the "Limit to workflows" multi-select. Templates aren't
instantiated runtime entities, so they never appear in event payloads
and scoping a sub to one is a no-op.

The chip-label pre-fetch is intentionally left unfiltered so previously-
saved IDs (or workflows later flipped to templates) still resolve to a
name instead of showing a bare CUID.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests for the new code introduced in 5a3a3893:

webhook-form (4 new tests):
- Renders the Scope section with agent + workflow labels
- Edit-mode prefetch resolves stored CUIDs to chip names via the
  admin agents/workflows endpoints (no `q` search query)
- Empty-scope create form skips the prefetch entirely (no wasted request)
- Workflow loader applies `isTemplate=false` so templates are filtered
  out server-side

webhooks-table (3 new tests):
- Scoped badge renders for a row with agentIds, tooltip shows counts
- Scoped badge renders for a row with workflowIds
- Scoped badge does NOT render when both arrays are empty (backward-compat
  rows stay un-decorated)

Restores webhook-form.tsx to ≥80% on all four coverage metrics (was
70.6/77.6/73.8/68.2 before, now 91.9/86.7/85.7/89.2). webhooks-table.tsx
branches improved 70.6 → 76.5; the residual gap is in pre-existing
`it.todo` cases unrelated to entity scoping (lines 105, 125, 268, 303).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sunrise Ready Ready Preview, Comment May 23, 2026 9:35pm

@JohnD-EE
Copy link
Copy Markdown
Contributor Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@JohnD-EE JohnD-EE merged commit 464cc94 into main May 23, 2026
6 checks passed
@JohnD-EE JohnD-EE deleted the feat/scoped-event-subscriptions branch May 23, 2026 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant