Repository navigation
[P2] Webhook: the spec WebhookSchema authoring surface is disconnected from the sys_webhook dispatcher #3461
Description
Activity
- added a commit that references this issue
on Jul 25, 2026 - added a commit that references this issue
on Jul 25, 2026 Liveness enrollment landed (#3485,
189854c) — this does not close the decision here. The systemic follow-up #3462 asked forwebhookto enter the liveness GOVERNED set; that's done, and it's deliberately neutral on this issue's A-vs-B choice.What the enrollment locks in, and the evidence it captured while re-confirming the disconnect at HEAD:
- The disconnect is confirmed, not just naming drift. The runtime
AutoEnqueuerreads onlysys_webhookdata rows (plugins/plugin-webhooks/src/auto-enqueuer.ts:175,where: { active: true }); there is no seeder and zeroinsert('sys_webhook')anywhere. Authoredwebhooks:never becomes a dispatchable row. - The dead-end registration trap.
objectqldoes "ingest"webhooks:—engine.ts:1183/:1343register each as a generic in-memory metadata item under type'webhook'— but nothing reads those items back, and they are not thesys_webhooktable. This is why the surface looks connected on a shallow read; the ledger_notecalls it out so it isn't mistaken for a working bridge. - Stale comments worth fixing when A/B is decided.
sys-webhook.object.ts:46("Authored viadefineWebhook()in code or the Studio editor") is aspirational for the code half — there's no path from adefineWebhook()artifact to a row. Andsys-webhook.object.ts:13-18attributes the loader toservice-automation'shttp_requestnode; the real consumer isplugin-webhooks'AutoEnqueuer. - The mapping table is pre-written.
packages/spec/liveness/webhook.jsonclassifies all 16 authorable propsdead(+authenticationexperimental), and each note records the salvageablesys_webhookcolumn (object→object_name,isActive→active,definition_json-only for headers/secret/timeoutMs) vs the no-sink-anywhere props (body/payloadFields/includeSession/retryPolicy/tags). Option A's materializer already has its field map here. - Interim author protection is live now.
os compilewarns thatwebhooks:is a silent no-op (one warning per webhook) — the same heads-up option B proposed as an interim marker, but without touching the schema, so it's neutral on the outcome.
When A lands, flip the mapped props to
live(cite the materializer) and registerwebhookas a real metadata type; when B lands, the ledger is removed with the schema. Leaving this open to track that.- The disconnect is confirmed, not just naming drift. The runtime
Resolved via Option A (build the bridge) in #3489 — Option B would retire a public authoring surface the showcase already uses, and the registry decomposition side already exists so A was the smaller build than it first appeared.
What the fix does
bootstrapDeclaredWebhooks(in@objectstack/plugin-webhooks) reads declaredwebhookmetadata from the ObjectQL registry — where the manifest decomposition already parksstack.webhooksas typewebhook(engine.tsmetadataArrayKeys) — validates each throughWebhookSchema.parse()(the spec schema now has a real consumer), and materializes asys_webhookrow at boot:object → object_name,isActive → active, full envelope →definition_json. Modeled on the siblingbootstrapDeclaredSharingRules.- Materialization runs on the data engine alone, before the auto-enqueuer's first refresh — intentionally not gated behind the realtime/messaging dispatch prerequisites, so a realtime-less deployment can't reproduce the silent no-op.
- Seed-not-clobber provenance (mirrors
sys_sharing_ruleAudit sibling declared-metadata↔record two-store types (sys_position, sys_sharing_rule, sys_capability) per ADR-0094 addendum #2909):sys_webhookgainsmanaged_by/customized; declared webhooks re-seed aspackage, but admin-created/edited rows are never clobbered.
Also surfaced during the fix: the showcase authored
webhooks:but never required thewebhookscapability, soWebhookOutboxPluginwasn't even mounted — a second layer of the same disconnect. The showcase now requireswebhooks+realtime, and ships the demo webhook inactive (placeholder endpoint).Verified: 9 unit tests (red-proofed) + a real
objectstack devboot — declared webhook materializes into asys_webhookrow, same-DB reboot is idempotent, and an admin'scustomizededit survives redeploy.Deliberately out of scope (tracked separately):
- Connector
webhooksremain not-yet-enforced (Audit: several event/subscription/connector enums are schema-only (declared, no runtime consumer) #3197) — not bridged here. - Registering
webhookas a first-class metadata type + enrolling it in the livenessGOVERNEDset (the systemic guard that would have caught this) — follow-up. - Field-level inertness of
retryPolicy/payloadFields/includeSession/authenticationis the [P0] Metadata property liveness audit: ~half of all spec properties are dead; a cluster of security props is parsed-but-unenforced #1878 domain; this bridge fixes "the definition reaches the dispatcher", not "every field is executed".
- added a commit that references this issue
on Jul 25, 2026 - added a commit that references this issue
on Jul 27, 2026 - added a commit that references this issue
on Sep 28, 2026
Split out from the naming-drift issue #1891 (webhook row) after a current-state investigation. Umbrella #1878.
What the audit called "naming drift" is actually a full disconnect
#1891listed webhook asobject → object_name/isActive → activenaming drift (spec key ≠ consumed key). On investigation it is not a live consumer reading a differently-named key — it's that the entire specWebhookSchemaauthoring surface is disconnected from the runtime dispatcher:WebhookSchema(packages/spec/src/automation/webhook.zod.ts:81,119) declaresobject/isActive. It's authored inside a Stack (stack.zod.ts:261 webhooks: z.array(WebhookSchema)) and by connectors (connector.zod.ts:271WebhookConfigSchema). It is not a registered metadata type (absent fromkernel/metadata-type-schemas.ts).sys_webhookdata object (plugins/plugin-webhooks/src/sys-webhook.object.ts), whose columns areobject_name(:112) andactive(:53).AutoEnqueuerreadsrow.object_name(auto-enqueuer.ts:267) andwhere: { active: true }(:176). These rows are admin-authored via the object's CRUD UI (sys-webhook.object.ts:43-44— "so admins can at least toggleactiveand edit simple URL/method fields without round-tripping through code"), and self-healed onsys_webhook:changed.WebhookSchema(object/isActive) into asys_webhookrow (object_name/active). Connectorwebhooksare explicitly "not yet enforced — never read at registration" (connector.zod.ts:662, Audit: several event/subscription/connector enums are schema-only (declared, no runtime consumer) #3197). So authoringwebhooks:on a stack produceswebhookmetadata artifacts that nothing materializes into dispatchable rows.Consequence: a parse-time
object → object_namealias onWebhookSchema(the "quick fix") would be inert — there is no consumer of the spec field to bridge to. Shipping it would be a false fix (ADR-0078). That's why this is split out rather than closed under #1891.Decision needed (pick one)
WebhookSchemaintosys_webhookrows, mappingobject → object_name,isActive → active,triggers/url/method/secret/… at that boundary. Closes the disconnect and the naming drift in one place. Overlaps Audit: several event/subscription/connector enums are schema-only (declared, no runtime consumer) #3197 (connector webhooks never read at registration). Larger; needs a home for the seeder (metadata ingestion or the webhooks plugin).sys_webhook(admin-authored rows) as the single source of truth; demote/removestack.webhooks+WebhookSchema(breaking — public export; needs a major + migration note), or mark it clearly[EXPERIMENTAL — not enforced / author sys_webhook rows instead]in the interim.Recommendation: A if declarative/stack-authored webhooks are on the roadmap (it's the ADR-0078-correct end state); otherwise B's interim marking so authoring
webhooks:isn't a silent no-op.Related
webhookin the livenessGOVERNEDset would have caught this automatically (see the GOVERNED-enrollment follow-up).