Docs

Events & Webhooks

Sixb has two event surfaces. Reach for domain events to read, stream, or audit what happened inside a project. Reach for webhooks to receive deliveries from an external provider.

  • Domain events — an append-only log of everything the runtime does (objects changed, telemetry appended, runs started and finished, and so on). Access them through sixb.events.
  • Webhooks — inbound HTTP endpoints owned by a connector that receive provider deliveries and run a handler.

The event log API is an observability surface, not a trigger API. Appending an event does not directly run a rule or workflow. Declarative event schedules use event selectors, but they are defined and registered separately from manual event appends.

Domain events

Every event shares a common envelope and carries a typed payload. The full set comes from the core event registry.

Event catalog

TypeTopicPayload highlights
object.createdobjectsobjectTypeId, primaryId, properties, propertyChanges
object.updatedobjectsobjectTypeId, primaryId, properties, propertyChanges
object.deletedobjectsobjectTypeId, primaryId, propertyChanges
telemetry.appendedtelemetryobjectTypeId, objectId, propertyId, value, unit?, at
link.createdlinkssourceTypeId, sourceId, linkId, targetTypeId, targetId, properties?, propertyChanges
link.updatedlinkssourceTypeId, sourceId, linkId, targetTypeId, targetId, properties?, propertyChanges
link.deletedlinkssourceTypeId, sourceId, linkId, targetTypeId, targetId, propertyChanges
action.requestedactionsactionId, subject, params, runId
action.completedactionsactionId, runId, subject, finishedAt
action.failedactionsactionId, runId, subject, error, finishedAt
schedule.triggeredschedulesscheduleId
rule.triggered / rule.resolvedrulesruleId, subject
sync.run.started / sync.run.finishedsyncssyncId, runId
pipeline.run.* (started, step.started, step.finished, finished)pipelinespipelineId, runId
workflow.run.* (queued, started, node.started, waiting, node.waiting, node.finished, finished)workflowsworkflowId, runId
workflow.intervention.* (requested, submitted, cancelled, expired)workflowsworkflowId, runId
dataset.version.committeddatasetsdatasetId

object.upserted, link.upserted, and link.removed are legacy compatibility events. Prefer created, updated, and deleted selectors for new code.

EVENT_TYPES and EVENT_TOPICS are exported from @sixb/core for the canonical lists at runtime.

Event envelope

Every stored event is a StoredDomainEvent: the EventEnvelope fields below, plus the event's type, topic, partitionKey, payload, and a broker cursor.

FieldTypeNotes
idstringUnique event id
schemaVersion1Envelope schema version
projectIdstringOwning project
occurredAtstringISO timestamp
correlationIdstring?Groups related events
causationIdstring?The event that caused this one
idempotencyKeystring?De-duplicates appends
actor{ type, id }?user, service, or system
metadataRecord<string, JsonValue>?Free-form context
cursorstringBroker position; pass as afterCursor to page forward

By default events are kept as a short recent log (the built-in stream retains the last two days), not a permanent history. A custom broker stream can change the retention window.

Reading events

sixb.events.read(input) returns stored events oldest-first, optionally filtered by topics and/or types and paged with afterCursor.

TS
const recent = await sixb.events.read({
  types: ["object.updated", "telemetry.appended"],
  limit: 100,
})

for (const event of recent) {
  console.log(event.type, event.cursor)
}

sixb.events.latestCursor() returns the newest retained cursor without loading event payloads. It is intended for efficient subscription baselines and returns undefined when the retained stream is empty.

OptionTypeNotes
topicsreadonly Topic[]Filter by topic (e.g. "objects")
typesreadonly Type[]Filter by event type
afterCursorstringReturn events after this cursor
limitnumberMax events to return

Subscribing to events

sixb.events.subscribe(input, handler) streams new events as they are appended and returns an unsubscribe function. Handler errors are swallowed to preserve fire-and-forget delivery.

TS
const unsubscribe = await sixb.events.subscribe(
  { types: ["action.completed", "action.failed"] },
  (events) => {
    for (const event of events) {
      console.log("invoice action finished", event.payload)
    }
  }
)

// later
unsubscribe()

In browser apps, use the @sixb/client event builder and React hooks instead of hand-written predicates. See Client events. For action buttons, prefer useActionRunMutation unless the screen needs broader live coordination.

Appending events

sixb.events.append(input) writes one or more events. The runtime emits domain events for you — append manually only when you model your own domain activity.

TS
await sixb.events.append({
  actor: { type: "service", id: "erp-sync" },
  events: [
    {
      type: "object.upserted",
      payload: {
        objectTypeId: "invoice",
        primaryId: "INV-1042",
        properties: { status: "paid" },
      },
    },
  ],
})

HTTP: GET /api/events

The server exposes the same log over HTTP. Results are authorization-scoped: a request only sees events the caller is permitted to read (see Authorization).

BASH
curl "http://localhost:3000/api/events?type=object.upserted&limit=50"
Query paramNotes
topicOne topic from EVENT_TOPICS
typeOne type from EVENT_TYPES
afterCursorPage forward from a cursor
limitMax events to return

The response is { count, events }. Events filtered out by scope are simply omitted; a caller with no permission gets 403.

Webhooks

A webhook is an inbound HTTP endpoint defined on a connector. The server owns route registration and dispatch; you own verification, parsing, and handling.

Define one with defineWebhook(id) and attach it to the connector's webhooks array.

TS
import { defineConnector, defineWebhook } from "@sixb/core"
import { createAcmeErpClient } from "../lib/acme-erp"

const WEBHOOK_SIGNATURE = "acme-local-secret"

export const acmeErpConnector = defineConnector("acme-erp", {
  type: "acme-erp",
  webhooks: [
    defineWebhook("invoice-events")
      .post()
      .json({ parse: parseInvoiceWebhookEvent })
      .verify(({ request }) => {
        if (request.headers.get("x-acme-signature") !== WEBHOOK_SIGNATURE) {
          throw new Error("Invalid Acme webhook signature")
        }
      })
      .idempotencyKey(
        ({ body, request }) => request.headers.get("x-acme-delivery") ?? body.deliveryId
      )
      .handle(({ body }) => {
        console.log(`received ${body.type} for ${body.invoiceId}`)
      }),
  ],
  connect() {
    return createAcmeErpClient()
  },
})

Each webhook is registered at POST /api/webhooks/<connectorId>/<webhookId>. The builder steps:

StepPurpose
.post()Method (webhooks are POST-only)
.json(schema?) / .text() / .raw()Body format and parser; schema.parse(value) validates and types the body
.verify(ctx)Optional; runs on raw bytes before parsing — throw to reject (401)
.idempotencyKey(ctx)Optional; returns a stable delivery id, or null/undefined to skip de-duplication
.handle(ctx)Required; runs the side effect, may return a WebhookResponse

The handler context carries the parsed body, the request, connector metadata, and a lazy client() that connects the outbound connector only if the handler needs it.

Dispatch lifecycle

For each delivery the server runs these steps in order:

  1. Verify — call verify with raw bytes. Failure returns 401.
  2. Parse — decode by body format and run the parser. Failure returns 400.
  3. Claim — resolve the idempotency key and claim the delivery. A duplicate or in_progress claim short-circuits to 202 Accepted without re-running the handler.
  4. Handle — run handle. A throw marks the delivery failed (so the provider's next retry can re-attempt) and returns 500.
  5. Complete — mark the delivery complete only after the handler succeeds.

When the handler returns nothing, dispatch responds 202 Accepted; otherwise it applies the returned status, headers, and body. Every delivery is recorded as a webhook run for observability (visible in the UI under the connector). Claiming and completion require webhook delivery storage to be configured.

Search docs

Search the documentation