Events

Every state change you act on is appended to one durable per-org log. Webhooks and 2 server-sent event streams carry it.

Each event is an envelope { id, type, seq, createdAt, data }. seq is the row's creation time in milliseconds, so it increases within an org, is not dense and can be fractional: order by it, dedupe by id.

Webhook delivery

A delivery is a POST of the envelope with Belora-Event, Belora-Delivery and Belora-Signature: t=<unix seconds>,v1=<hex>. v1 is the lowercase hex HMAC-SHA256, keyed with the endpoint secret, of t + "." + raw body. Reject a t more than 300 seconds from your clock.

signal.clustered

A signal was clustered into a theme

data

signalIdstring
themeIdstring

decision.created

A decision was opened and awaits a person

data

decisionIdstring
themeIdstring
questionstring
optionsstring[]

decision.resolved

A decision received its verdict

data

decisionIdstring
themeIdstring
chosenIndexinteger
optionstringThe label of the chosen option
rationalestring
decidedBystringThe person who decided: an auth id for the console and API doors, a Slack user id for Slack, Belora for an auto-applied decision
door"api" | "console" | "slack"Which surface carried the verdict

job.created

A job was queued for a runner to claim

data

jobIdstring
repostringThe repository the job targets, as owner/name
decisionIdstring | nullThe decision the job implements; null when it follows a theme
linkedJobIdsstring[]Jobs already queued for the same decision, claimed together across repositories

job.completed

A job closed with a recap; the grade is what its pull request said at that moment

data

jobIdstring
themeIdstring | null
gradeobject | nullNull when the closer is not an executor kind that is graded

job.failed

A job failed for good: the retry cap, a closing that never finished, or a claim too large to run

data

jobIdstring
themeIdstring | null
reasonstring

theme.shipped

A theme shipped: every pull request it recorded is merged

data

themeIdstring
titlestring
prsobject[]
prs[].repostringowner/name
prs[].numberinteger
prs[].urlstring
changedstring[]One line per changed file from the recaps of the theme's jobs, as path: what
recapSynthesizedbooleanBelora wrote at least one of those recaps from the diff because the executor sent none

theme.merged

A theme was merged into another and archived

data

themeIdstringThe theme that was absorbed
intoThemeIdstring

decision.escalated

An unanswered decision, or the question of a paused job, was nudged to its owner or re-routed to the org owner

data

decisionIdstring | nullThe decision; null when a paused job's question escalated
jobIdstring | nullThe paused job; null when a decision escalated
step"nudge" | "reroute"
tostring | nullThe person addressed; null when a nudge has nobody with a verified identity to reach

job.expired

A job lost its lease or stalled in closing, and was requeued or failed

data

jobIdstring
outcome"requeued" | "failed"
reason"lease" | "closing"
attemptsinteger
personIdstring | nullThe person whose API key held the job; null for the runner

Streams

GET /v1/events/stream

Server-sent events of the org's event log, resumable with Last-Event-ID; a stream lasts at most 280 seconds, so reconnect

signal.clustereddecision.createddecision.resolvedjob.createdjob.completedjob.failedtheme.shippedtheme.mergeddecision.escalatedjob.expired

GET /v1/sessions/{id}/events

Server-sent events of one session's messages and commit, resumable with Last-Event-ID; a stream lasts at most 280 seconds, so reconnect

session.messagesession.commitsession.concluded