| Field | Value |
|---|---|
| RFC | 0127 |
| Title | Streaming & CDC trigger sources |
| Status | Accepted |
| Author(s) | openwop-app maintainers |
| Created | 2026-07-05 |
| Updated | 2026-07-06 (Active → Accepted — single-witness graduation (bootstrap steward waiver), tier-1 reference-host evidence (openwop-app, Cloud Run rev openwop-app-backend-00411-77q): both stream and change sources ingest non-vacuously through the RFC 0083 four-state machine with a schema-valid TriggerEvent envelope and a content-free trigger.delivery.attempted (SR-1), witnessed by @openwop/openwop-conformance@1.53.1 trigger-stream-cdc-sources.test.ts under OPENWOP_REQUIRE_BEHAVIOR=true and steward-curl-verified on the wire (stream (topic,partition,offset) + change op present; body-canary absent from the durable event). Host advertises honest-off — stream/change stay OUT of triggerBridge.sources[]/ingestion.externalSources[] until it operates a real broker/CDC consumer. Register swept; G4 carried forward = second witness is the first host operating a real streaming/CDC consumer (tier-2 MyndHyve lacks the surface — 0124-G6 analog). Draft→Active earlier the same day; §2/§3 amended for schema fidelity at surface-landing) |
| Affects | spec/v1/trigger-bridge.md, spec/v1/capabilities.md, schemas/trigger-subscription.schema.json, schemas/trigger-event.schema.json, conformance trigger-bridge-* scenarios |
| Compatibility | additive per COMPATIBILITY.md |
| Supersedes | — |
| Superseded by | — |
Summary
RFC 0083 defined a durable trigger bridge whose capabilities.triggerBridge.sources[] enumerates webhook / schedule / queue / email / form, and RFC 0099 defined the normalized TriggerEvent envelope for externally-originated events. This RFC additively extends both with two new sources — stream (a message consumed from a streaming broker: Kafka / Kinesis / Pub-Sub) and change (a change-data-capture row from a warehouse/database changelog) — so a CDP-class host can advertise honest, conformance-gated streaming/CDC ingestion. It reuses RFC 0099's TriggerEvent envelope, SSRF posture, and body-redaction invariants unchanged; only the source vocabulary and a small per-source ingestion advertisement grow.
Motivation
Customer-Data-Platform and analytics hosts ingest first-party events from streaming brokers and warehouse CDC feeds, not only webhooks/email/forms. Today a host that consumes a Kafka topic or a BigQuery/Snowflake changelog and lands each message as a run has no honest wire vocabulary for it: capabilities.triggerBridge.sources[] cannot name stream or change, so the host must either mislabel the source as webhook (a dishonest wire claim that OPENWOP_REQUIRE_BEHAVIOR=true rightly rejects) or leave the capability unadvertised (invisible to peers/agents). The ingestion plumbing is a host implementation detail; the source vocabulary and its ingestion contract are wire shape and therefore belong in the spec (per CONTRIBUTING.md — a new capability value is a normative change).
Proposal
1. source vocabulary (additive)
Extend the trigger source enum (RFC 0083 sources[], RFC 0099 TriggerEvent.source, TriggerSubscriptionRegistration.source) with two values:
"source": {
"type": "string",
- "enum": ["webhook", "schedule", "queue", "email", "form"]
+ "enum": ["webhook", "schedule", "queue", "email", "form", "stream", "change"]
}
stream— a message consumed from a streaming broker (Kafka/Kinesis/Pub-Sub). The
host is the consumer; each delivered message normalizes to one TriggerEvent.
change— a change-data-capture record (insert/update/delete) from a warehouse or
database changelog. Carries an op discriminator in metadata.triggerData.
Both are OPTIONAL sources: a host that consumes neither simply omits them from capabilities.triggerBridge.sources[] (the RFC 0083 §D "any one durable source" profile is unchanged — stream/change are additional durable sources, not required ones).
2. TriggerEvent envelope — same pattern, two per-source sub-objects
A streamed/CDC event uses the existing RFC 0099 TriggerEvent envelope pattern: the envelope IS the in-run ctx.triggerData payload, it carries exactly the per-source sub-object matching its source (RFC 0099 §F.1), and it never appears on the durable event log — the trigger.* events stay content-free (SR-1). Two new $defs in trigger-event.schema.json:
StreamEvent(present iffsource == "stream"):topic/partition/offset/
key / message — the broker coordinates MAY feed at-least-once dedup keying.
ChangeEvent(present iffsource == "change"): **op(`"insert" | "update" |
"delete") is REQUIRED** (schema-enforced), plus table / changelogId / before / after` row images.
(Amended 2026-07-06 for schema fidelity: the original draft placed op in a metadata.triggerData bag that does not exist on the wire — the envelope's per-source sub-object is the real home, and it makes the op requirement schema-expressible.)
3. Ingestion advertisement (additive enum values)
capabilities.triggerBridge.ingestion.externalSources[] (RFC 0099 §F.3 — the real advert is an array, not per-source booleans; amended 2026-07-06 for schema fidelity) gains the two values — a host MUST list a source there only when it behaviorally ingests it (the RFC 0099 honesty rule):
"ingestion": {
- "externalSources": ["webhook", "email", "form"]
+ "externalSources": ["webhook", "email", "form", "stream", "change"]
}
Direction inverts for the new sources: the host is the consumer — there is no inbound ingest URL, so a §F.2 registration for stream/change returns an empty binding ({}); the broker/warehouse connection is a credential-brokered egress (RFC 0095), never a wire field.
4. Dedup, SSRF, replay — reused verbatim
- Dedup. A
streamevent's dedup key SHOULD derive from the broker `(topic, partition,
offset); a change event's from (table, changelog-id)`. Reuses the RFC 0083 §C-1 ≥24h dedup floor — no new mechanism.
- SSRF. Any host-side fetch the ingestion path performs (e.g. resolving a schema-registry
reference) MUST pass the RFC 0099 SSRF guard. The broker/warehouse connection is a credential-brokered egress (RFC 0095), never a wire field.
- Replay.
trigger.*events stay content-free recorded-facts (RFC 0083/0099); a
streamed/CDC event introduces no new non-determinism on the wire (the body is host-local).
Positive example
capabilities.triggerBridge = { supported: true, sources: ["webhook","schedule","queue","email","form","stream","change"], ingestion: { externalSources: ["webhook","email","form","stream","change"] }, dedup:true } — a CDP host that consumes a Kafka topic and a warehouse CDC feed, both landed as durable runs.
Negative example
A host advertising sources: [...,"stream"] but whose trigger-bridge-delivery scenario never produces a trigger.delivery.attempted for a source:"stream" event fails the gated conformance run under OPENWOP_REQUIRE_BEHAVIOR=true (advertise-only-what-you-honor).
Security & Compatibility
- Compatibility: additive. New enum values + new optional advertisement booleans. Pre-RFC
hosts/peers ignore unknown sources; the four-state subscription machine, dedup/retry model, and causation edge are unchanged.
- SR-1 preserved. No message/row body ever reaches the wire — the existing RFC 0099
redaction invariant covers stream/change unchanged (the payload builder's closed allowlist already excludes triggerData).
Host implementation note (non-normative)
The consuming plumbing (core.openwop.streams connection + node pack landing each message via ingestExternalEvent) is host-extension and needs no wire change beyond this RFC's source value. openwop-app ADR 0269 (CDP-G) gates its streaming-ingest work on this RFC reaching Accepted.
Resolved questions
1. change stays ONE source with an op discriminator (not three insert/update/delete sources). This mirrors how webhook carries an HTTP method inside one source rather than fragmenting the sources[] vocabulary per verb — keeping the enum small and stable is the dominant concern. op ("insert" | "update" | "delete") is REQUIRED in a change event's metadata.triggerData (§2). (Resolved 2026-07-06.) 2. Reuse the RFC 0083 dedup floor; no deliveryGuarantee advertisement is added. An at-least-once/exactly-once advert would be a normative claim a conformance run cannot refute over the wire (the guarantee is a property of the host's consumer + the broker, not an observable event field) — an unfalsifiable MUST is worse than none. The observable, testable contract is the existing dedup floor (trigger.delivery.attempted + the ≥24h dedup key). A deliveryGuarantee sub-field is deferred until a concrete conformance gap demonstrates the dedup floor is insufficient. (Resolved 2026-07-06.)
Open questions
None — the two questions above are resolved. The RFC is ready for the comment window.