Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3.4 Events

Events are the vocabulary of a Beverly application.

They describe what can happen and what has happened.

event! declares ordinary Bevy Message structs in namespaces. Registration and handling remain explicit application responsibilities:

#![allow(unused)]
fn main() {
use bevy::prelude::*;
use beverly::prelude::*;

event! {
    User::Create {
        name: String
    }
    User::Delete {
        id: u64
    }
}

fn setup(mut app: App) {
    app.add_message::<User::Create>()
        .add_message::<User::Delete>();
}
}

Write and read them with Bevy’s normal message APIs:

#![allow(unused)]
fn main() {
fn create_user(mut messages: MessageWriter<User::Create>) {
    messages.write(User::Create {
        name: "Alice".to_string(),
    });
}

fn handle_users(mut messages: MessageReader<User::Create>) {
    for message in messages.read() {
        info!(name = %message.name, "creating user");
    }
}
}

The macro declares types only. It does not register messages, route commands, or emit events automatically; those behaviors stay explicit in the systems and plugins that own the application contract.

Beverly uses two kinds of Events:

  • Commands — things you want to happen.
  • Facts — things that happened.
Command
    │
    │ "I want this to happen."
    ▼
 Application
    │
    │ "This happened."
    ▼
Fact

This distinction gives applications a simple, predictable language for describing behavior.

Commands

A Command expresses an intention.

It tells the application what you want it to do.

#![allow(unused)]
fn main() {
event! {
    User::Create {
        name: String
    }

    User::Delete {
        id: UserId
    }
}
}

These Events describe actions the application understands:

User::Create
User::Delete

A human can trigger them.

An API can trigger them.

Another application can trigger them.

An AI agent can trigger them.

The Event is the common interface.

There is no separate API for AI agents to learn.

Facts

A Fact describes something that has already happened.

#![allow(unused)]
fn main() {
event! {
    User::Created {
        id: UserId
    }

    User::Deleted {
        id: UserId
    }
}
}

The distinction is simple:

CreateUser
    = Command
    = "Create this user."

UserCreated
    = Fact
    = "This user was created."

Commands express intent.

Facts express reality.

This makes it possible for different parts of the application to react to the same facts without needing to know which component originally caused them.

Events Are Strongly Typed

Events are defined as typed Rust contracts.

#![allow(unused)]
fn main() {
event! {
    User::Create {
        name: String
    }
}
}

The compiler knows what User::Create requires.

An invalid payload is not a runtime mystery.

It is a type error.

Rust catches malformed Events at compile time.

This gives Events a property that is particularly valuable for AI-generated code:

The compiler can tell the AI when it generated something that doesn’t match the application’s contract.

The same type system that protects ordinary Rust code protects the application’s event vocabulary.

Events as Self-Documenting Code

An Event definition tells you what the application can do without requiring you to read the implementation first.

#![allow(unused)]
fn main() {
event! {
    Document::Summarize {
        id: DocumentId
    }

    Document::Delete {
        id: DocumentId
    }

    Document::Publish {
        id: DocumentId
    }
}
}

You can glance at this and immediately understand three capabilities of the application.

That is intentional.

Beverly aims to reduce boilerplate by making the code itself the documentation.

Instead of maintaining separate lists of:

  • API endpoints
  • AI tools
  • event names
  • documentation
  • schemas
  • permissions
  • telemetry definitions

the Event becomes the authoritative definition from which these things can be derived.

The best documentation is the code that cannot lie.

Events Are an Interface for AI

This becomes especially powerful when an AI agent participates in the application.

The agent doesn’t need to understand the entire application implementation.

It needs to understand the application’s vocabulary.

For example:

User::Create
    name: String

User::Delete
    id: UserId

Document::Summarize
    id: DocumentId

The agent can discover these Commands and determine what actions are available.

To perform an action, it simply issues the corresponding Command.

Agent
  │
  │ User::Create { name: "Alice" }
  ▼
Controller
  │
  ▼
Model

The agent doesn’t need a special Agent::CreateUser interface.

It uses the same application vocabulary as everything else.

Agents can also listen for Facts:

UserCreated
DocumentPublished
PaymentCompleted

An agent can respond when the Fact it cares about occurs.

UserCreated
    ↓
Agent observes Fact
    ↓
Agent decides what to do
    ↓
Agent issues Command

This produces a very simple agent loop:

Listen for Facts. Decide. Issue Commands.

Events and Observability

Beverly Events are also observable by design.

Under the hood, Beverly’s Event system integrates with OpenTelemetry so that Events can carry standardized telemetry information as part of their normal lifecycle.

That means application activity can be observed without developers having to build a separate logging architecture around every action.

Conceptually:

Event
  ↓
Action
  ↓
State Change
  ↓
Telemetry
  ↓
Event History

The important idea is that the application already knows what happened.

The Event is the natural unit of application activity.

Logging doesn’t need to be a separate thing that developers remember to add everywhere.

Playback and Rewind

Because Events form an ordered history of application activity, playback becomes remarkably straightforward.

Instead of trying to reconstruct what happened from scattered logs, you can step through the application’s Event history:

Event 001
   ↓
Event 002
   ↓
Event 003
   ↓
Event 004
   ↓
Current State

Replaying the application means replaying its Events.

Rewinding means moving back through that history and reconstructing the corresponding state.

This creates an important capability for AI-native applications.

When an agent changes something, its actions become part of the same observable application history as human actions.

A developer can inspect:

Fact: DocumentCreated
Command: Document::Summarize
Fact: DocumentSummarized
Command: Document::Publish
Fact: DocumentPublished

The AI’s behavior is no longer hidden behind a collection of opaque tool calls.

It becomes part of the application’s history.

If the application records what happened, the application can show you what happened.

How Events Are Stored

The application state you see at any moment is not the source of truth.

The Event history is.

Event Log (append-only)
   │
   ├── 001  Fact: DocumentCreated   { id: 1 }
   ├── 002  Command: Document::Summarize { id: 1 }
   ├── 003  Fact: DocumentSummarized { id: 1 }
   ├── 004  Command: Document::Publish { id: 1 }
   └── 005  Fact: DocumentPublished { id: 1 }

Every Event is written to this log in the order it occurred, and it is never edited or removed once written.

Each entry carries the information required to reproduce it later:

Sequence   → position in history
Timestamp  → when it happened
Event      → the typed Command or Fact
Payload    → its strongly typed data

The Model’s current state is simply what you get from folding the log from the beginning:

State(0) + Event 001 + Event 002 + ... + Event N = Current State

Because the log is append-only, this is the same guarantee databases call an event-sourced or write-ahead log: history is immutable, and current state is a derived, disposable projection of it.

The log is the truth. The current state is just a cache of it.

Saving Event History

Saving an application’s history means saving its Event log, not its in-memory Models.

Event
  ↓
Serialize (typed payload → bytes)
  ↓
Append to log (disk, database, or stream)
  ↓
Acknowledge

Because Events are strongly typed Rust values, they serialize the same way any other typed Rust data does. There is no bespoke save format to invent per feature.

The log can live wherever it makes sense for the application:

Event Log
   │
   ├── a local file
   ├── SQLite / Postgres
   ├── an append-only object store
   └── a distributed log (e.g. Kafka-style stream)

Saving does not require freezing the whole application. Only the new Events since the last save need to be appended, which is what keeps this cheap even for long-running applications.

Persistence is just “keep appending.” Nothing about the Model needs a separate save/load path.

Replaying

Replaying means reading the Event log from the start and re-applying every Event, in order, to rebuild state.

Empty State
   │
   ├── apply Event 001
   ├── apply Event 002
   ├── apply Event 003
   ├── ...
   └── apply Event N
   ↓
Reconstructed State (identical to the original)

Because Facts describe what already happened, replaying them is deterministic: applying the same Events in the same order always produces the same resulting state.

This is useful in several concrete situations:

Restarting the application
    → replay the log to rebuild state from nothing

Debugging a production issue
    → replay a copied log locally to reproduce it exactly

Testing
    → replay a recorded scenario as a regression test

Migrating a Model
    → replay old Events through new logic to backfill new state

Nothing about “restoring the application” is special-cased. It’s the same replay used everywhere else.

Rewinding

Rewinding moves state backward by replaying only a prefix of the log, rather than the whole thing.

Event 001 → Event 002 → Event 003 → Event 004 → Event 005
                              ▲
                    rewind target: after Event 003

To rewind to a point in time, the application discards nothing. It simply re-derives state by folding the log up to that Event and stopping:

State(0) + Event 001 + Event 002 + Event 003 = State at that point in time

The Events after the rewind point are not deleted. They remain in the log. Rewinding is a read operation over history, not a mutation of it.

Rewind ≠ Undo-by-deleting
Rewind = Recompute state as of an earlier Event

This distinction matters. Because the full log still exists, an application (or a developer, or an agent) can rewind, inspect an earlier state, and then choose to either:

Resume forward
    → continue applying the remaining Events as normal

Branch
    → issue new Commands from that point, creating a new
      continuation of history instead of the original one

Rewinding shows you the past. It doesn’t erase it.

Stepping Through

Because the log is just an ordered list, moving through it one Event at a time is a simple index operation, not a special debugging feature bolted on afterward.

            ◀── step back            step forward ──▶

Event 001  Event 002  [ Event 003 ]  Event 004  Event 005
                            ▲
                      current cursor

A step forward applies the next Event in the log to the current state:

State(cursor) + Event(cursor + 1) = State(cursor + 1)

A step back recomputes state as of the previous Event, the same way rewinding does, just one Event at a time:

State(cursor − 1) = fold(Event 001 ..= Event(cursor − 1))

This gives an application (or a developer tool built on top of it) the same experience as stepping through a debugger, except the “breakpoints” are the application’s own Commands and Facts:

Step back  → Fact: DocumentSummarized
Step back  → Command: Document::Summarize
Step back  → Fact: DocumentCreated

Because every step is just a Command or Fact, stepping through history is also stepping through an audit trail. There is nothing else to reconstruct.

A step-through debugger for application behavior isn’t a separate feature. It’s the same Event log, read one entry at a time.

Snapshots

Replaying an entire log from the beginning is simple and correct, but for a long-running application it can become slow.

A snapshot solves this without changing anything about how replay, rewind, or stepping work conceptually:

State(0) + Events 001..500  = State at Event 500
                                     │
                                     ▼
                             Snapshot("at 500")

Snapshot("at 500") + Events 501..N = Current State

A snapshot is just a cached checkpoint: a Model’s state at a known Event, saved so replay doesn’t need to start from empty state every time.

Replay without a snapshot:
   Empty State + Events 001..10,000

Replay with a snapshot:
   Snapshot(9,900) + Events 9,901..10,000

Snapshots are an optimization, not a change to the model of history. The Event log remains the single source of truth; a snapshot can always be regenerated by replaying from the beginning.

A snapshot is a shortcut through history, never a replacement for it.

Less Boilerplate

Traditional applications often maintain multiple representations of the same capability.

A feature might require:

Implementation
API endpoint
Schema
Documentation
AI tool definition
Logging
Permissions
Tests

Beverly’s Event system is designed to collapse much of that duplication.

One strongly typed Event definition can become the source for the representations the rest of the application needs.

             Event Definition
                    │
       ┌────────────┼────────────┐
       ↓            ↓            ↓
    Rust Types    AI Tools    Documentation
       │            │            │
       ├────────────┼────────────┤
       ↓            ↓            ↓
   Validation   Permissions   Telemetry

The goal isn’t to generate more code.

It is to stop writing the same information multiple times.

Events Are the Contract

Events create a shared vocabulary between every participant in the application.

Human ────────┐
              │
Keyboard ─────┤
              │
API ──────────┤
              ├──► Command ──► Controller ──► Model
              │
AI Agent ─────┘

Model ──► Fact ──► View
              │
              ├──► Agent
              │
              ├──► Other Systems
              │
              └──► Event History

Commands tell the application what someone wants to happen.

Facts tell the application what actually happened.

The compiler makes sure those messages conform to their contracts.

OpenTelemetry makes their activity observable.

The Event history makes that activity replayable.

And the same vocabulary can be understood by both humans and AI agents.

Events turn application behavior into typed, observable, self-documenting vocabulary.

That is the foundation for Beverly’s larger idea:

One application. One vocabulary. Humans and AI use the same interface.