events/ — event ingestion
Unifies push (webhook) and pull (polling) sources behind a single contract: emit typed Signals into the host’s consequence engine. A signal type is a DomainType declared in types/ whose parents includes Signal.
The reference host implements both delivery modes — the poll sweep with signal-id dedup, and webhook delivery. Existing webhook receivers in webhooks/ continue to work in parallel.
Webhook event source
Section titled “Webhook event source”- type: StripeEvent # name of a signal type declared in types/ webhook: signature: hmac-sha256 # one of: hmac-sha256, hmac-sha1, jwt, none signature-secret: STRIPE_WEBHOOK_SECRET tenancy: path-token # one of: path-token, payload-field, header mapping: # type-property → JSONPath into the payload id: "$.id" occurredAt: "$.created" sourceKind: "stripe" sourceId: "$.id" eventType: "$.type" amount: "$.data.object.amount" currency: "$.data.object.currency" customerId: "$.data.object.customer" tier-when: # optional — override default tier INTERRUPTIVE: "{{ payload.type == 'charge.failed' }}" TIMELY: "{{ payload.type == 'invoice.upcoming' }}"The host receives the webhook, verifies the signature, resolves its registered world and context,
projects the payload through the mapping into a StripeEvent instance, and emits it as a Signal
into the consequence engine. The payload cannot select or override that route.
| Field | Required | Meaning |
|---|---|---|
type |
yes | Name of a type declared in this realm (or another loaded realm) whose parents includes Signal. |
webhook.signature |
yes | Signature scheme the host’s built-in verifiers handle. |
webhook.signature-secret |
conditional | Env-var name holding the shared secret. Required for any non-none scheme. Conventional realms only: a captured realm’s webhook channel names a declared credential in signature.credential (see TypeScript realms). |
webhook.tenancy |
yes | Strategy for routing the inbound webhook to a world. |
webhook.mapping |
yes | Map of type-property → JSONPath. Every required property of the Signal parent (id, occurredAt, sourceKind, sourceId) must be covered. |
webhook.tier-when |
no | Tier-override map. Each entry’s value is a Jinja boolean expression evaluated against the parsed payload. First true wins; default tier is AMBIENT. |
Polling event source
Section titled “Polling event source”For services without webhook support — or where polling is preferable — declare a poll block
instead of (or in addition to) webhook. Polling reuses the host’s task scheduler. Like a handler,
the source must be adopted before it can run. Its cursor identity is (worldId, contextId, adoptionId, sourceRegistrationId, type); the cursor record also carries the current principal,
realm, access-policy, grant, and credential revisions as fences. A principal, credential, or revision
change pauses polling. The host may resume the existing cursor only after proving it addresses the
same source stream; otherwise an operator must explicitly reset it. A reset is audited, and events
already persisted under the source’s stable event identity remain deduplicated. No change silently
starts a fresh cursor and replays the source.
- type: LinearIssue poll: every: 10m api: linear # the realm's already-learned API (apis/linear.yml) method: list_my_issues # method name on that API cursor: param: updated_after # the API parameter that takes the cursor value from: $.updatedAt # how to extract the next cursor from each result mapping: id: "$.id" occurredAt: "$.updatedAt" sourceKind: "linear" sourceId: "$.id" title: "$.title" state: "$.state.name" url: "$.url"| Field | Required | Meaning |
|---|---|---|
poll.every |
yes | Cadence (e.g. 5m, 1h). |
poll.api |
yes | Name of an API declared in apis/ (this or another realm). |
poll.method |
yes | Method name on that API. |
poll.args |
no | Arguments to the call. Jinja-templated against { cursor } only. World, context, principal, and credentials are host-bound and unavailable to the template. Methods such as list_my_issues derive the subject from the bound credential. |
poll.cursor.param |
no | API parameter the host populates with the persisted cursor. |
poll.cursor.from |
no | JSONPath into each returned result, used to compute the next cursor (the maximum value across the batch becomes the new cursor). |
poll.mapping |
yes | Same shape as the webhook mapping block. |
A realm may declare both webhook: and poll: for the same type — the host prefers webhook delivery and uses polling as a backstop for catch-up after downtime.
Why this matters
Section titled “Why this matters”Both sources produce Signals of the realm-declared type. From there, the consequence engine, triage rules, persistence (SignalRecord), notifications, and chat surfacing are all type-aware: signal.type.isAssignableFrom(StripeEvent) is a real predicate, not a string match.
No JVM bytecode is shipped — realms that need behaviour beyond mapping should expose it via actions/ (LLM-driven) or mcp/ (sandboxed servers).

