Skip to content

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.

events/stripe.yml
- 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.

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.

events/linear.yml
- 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.

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).