Concept · 2 of 7

Event

An event is a committed fact — the unit of reality in a weave system. Scrolls, workflows, and tools all exist to produce, store, or respond to events.

The substrate, not the log

It is tempting to read "event" as "a record of what happened." That is the soft version and it is wrong here. In weave there is no separate reality that events are the paper trail for — the commit is the happening. Nothing has occurred until an event lands on a scroll.

This reframes what state means. The struct in memory holding your agent's "current" conversation, tool state, or workflow position is not the truth. It is a cached projection of the events on the scroll. Lose the cache and you lose nothing; replay the events and the state returns. Lose the scroll and you lose everything.

It also reframes replay. Replay is not reconstruction from secondary records — it is re-derivation from the same substrate that produced the state the first time. That is why replay is exact and not approximate.

Anatomy

An event has a handful of fields. Topic and payload carry the meaning; offset and timestamp are the runtime's bookkeeping.

// The event type — in Go. Same shape in TS and Python.
type Event struct {
    Scroll   string    // which scroll this belongs to
    Offset   int64     // monotonic position, assigned at commit
    Topic    string    // e.g., "ai.response", "tool.result"
    Payload  []byte    // schema is defined per-topic
    At       time.Time // commit timestamp
}

The topic is the contract — subscribers key off it, codegen binds payload types to it, and routing is decided by it. The offset is what makes ordering meaningful and replay deterministic: every event has a place on its scroll that nothing else can take.

Emit is the act

When a tool handler returns, it doesn't "log" its result — it commits an event that is the result. The append is the act; the handler's return value, left uncommitted, means nothing to the rest of the system.

// A tool handler emitting its result.
// The Append call is the act — not a record of some earlier act.
if err := w.Scroll().Append(ctx, scroll.Event{
    Topic:   "tool.result",
    Payload: mustMarshal(result),
}); err != nil {
    return err
}

This is why weave doesn't expose a separate logging or messaging API. There is no second channel. Every meaningful effect is an event on a scroll, or it hasn't happened.

External commits and derived events

Every event falls into one of two kinds. The split is load-bearing — it is what makes replay work.

External commit

A fact the runtime had to reach outside itself to obtain. The model's response, the handler's output, a human's input. The runtime can't reproduce these deterministically — so it captures them on commit and keeps them forever.

  • ai.response what the model said
  • tool.result what the handler returned
  • user.input what a human typed
  • model.embedding what the embedding service returned
Derived event

A fact that follows mechanically from prior events plus the workflow definition. These are recomputable — if you have the externals and the definition, you can always produce the deriveds again.

  • ai.request what to ask, given the prompt + prior events
  • tool.dispatch what to invoke, given the model's tool call
  • fsm.state_change what state follows from the last one
  • workflow.step which step runs next in the workflow def
// scroll: run-abc123
[0]  ai.request       derived         { prompt: "greet the hero" }
[1]  ai.response      external        { text: "welcome, traveller" }
[2]  tool.dispatch    derived         { name: "speak", text: "welcome, …" }
[3]  tool.result      external        { ok: true, duration_ms: 42 }

Both kinds are first-class, real events. Derived events aren't shadows of the externals — they're the actual commits the runtime made to advance the system. The difference is only in where the fact came from. Replay captures externals and recomputes deriveds; that's why feeding recorded responses back in produces identical behavior.

Topics as contract

A topic is more than a label. It is the schema identity and the routing key at once. Subscribers name the topics they want; codegen binds typed payloads to each topic; the runtime rejects commits whose payload doesn't match its topic's schema.

Topic namespaces follow the substrate that produced them: ai.* for model-facing events, tool.* for handler-facing, workflow.* for state machine transitions, user.* for human-originated. Third parties adding new namespaces is fine — the registry is open, collision is managed by convention.

One topic per kind of domain fact

The rule that makes the rest of weave hold together: every topic names one kind of domain fact — one recognisable happening, named for what occurred. Not "a field was updated," but "a caravan was ambushed," "a dragon was slain," "a quest was posted."

Fields are a read-model artifact. How the current state of a caravan or a quest gets stored in a row is the projection's problem. Events are upstream of that — they're what happened, not how you chose to remember it. If your event names look like database patches (title_updated, field_set), you're designing the storage, not the domain.

// RIGHT: each topic names one kind of domain fact.
// Written in the weave DSL — .weave files codegen typed Go/TS/Python bindings.
event "caravan_ambushed" {
    caravanId  string
    location   string
    hour       string
    attackers  int
    escaped    int
}
event "dragon_slain" {
    dragonId   string
    slayer     string
    weapon     string
    witnesses  []string
}
event "fealty_sworn" {
    heroId     string
    liegeId    string
    oathText   string
    sworn_at   string
}

// Payloads can be rich. What matters is that each TOPIC describes
// one kind of domain fact — a happening with a recognisable name.

Payloads can be as rich as the fact requires. A caravan ambush has hour, location, attackers, casualties — all of it belongs on one event because it's describing one happening. What the event does not do is stand in for several different facts.

// WRONG: a generic "something changed" topic with a discriminator.
// event "quest_updated" {
//     field string  // "title" | "reward" | "location" | "deadline" | ...
//     value string  // whatever the payload of this particular fact was
// }
//
// This isn't one kind of fact — it's four or five different facts
// (title revised, reward increased, location moved, deadline extended)
// smuggled into one topic. Every consumer now has to switch on `field`
// to recover what actually happened. Topic routing is gone, payload
// typing is gone, and "the dead-line was extended" looks identical on
// the scroll to "the reward was increased" except for a string.
//
// The fix is to name each fact for what it is:
//   event "quest_title_revised"     { questId; newTitle;  revisedBy }
//   event "quest_reward_increased"  { questId; newAmount; reason }
//   event "quest_deadline_extended" { questId; newDate;   reason }

The shortest heuristic. If you need a switch on a payload field to decide what the event means, that switch is covering for multiple domain facts sharing one topic. Split the topic. One topic = one kind of fact = one meaning.

Why this matters. Topic-based routing, typed folds without branching, clear audit trails, free conflict detection — all of it depends on the topic meaning exactly one kind of fact. A discriminator is a silent agreement to give those properties up. The coincidence that one fact often maps to one field in some projection is downstream of the design, not a driver of it.

What events are not

  • Not log lines. Log lines are ad-hoc strings emitted alongside the work. Events are structured commits that are the work. Remove the log line and the system behaves the same. Remove the event and nothing happened.
  • Not tracing spans. Spans are derived views, reconstructed after the fact for observability. Events are the source; you can project spans out of events, but not the other way around.
  • Not commands. Events are past-tense facts — "a dispatch was committed." Even the ones that represent an intent to do something next (tool.dispatch) are facts about the intent having been committed, not imperative requests in flight.

Status

The event model is the most stable part of weave — topics, payloads, and offsets have been the shape for a long time. The surface around events (introspection, validation) is where the live work is.

Topic-based subscribe
implemented
Subscribers filter by topic; the runtime delivers only matching events.
Typed payloads (per language)
implemented
SDKs bind each topic to a typed struct in Go, TS, and Python — no untyped blobs at the application boundary.
Causation chain
sdk-shimmed
Events carry a causation id pointing to the event that caused them; the SDK threads it through, but the query surface for walking the chain is thin.
Schema registry
designed
Topic schemas live in the codegen today; a runtime-queryable registry for cross-service contract checks is on the roadmap.