The Birth of Event Continuations: The Night of August 27, 2025

· 10 min read

The Birth of Event Continuations

A zero-cost abstraction that disappears.

What Are Event Continuations?

When you invoke an event in Koru, you explicitly handle its branches and chain into the next step:

~validate(value: 42)
| valid v |> double(value: v.result)
    | ok doubled |> success(result: doubled.result)
| invalid e |> failure(msg: e.msg)

The syntax is elegant. But the magic isn’t in the syntax.

The magic is what it compiles to.

The Disappearing Continuation

Here’s a complete Koru program with event definitions and a flow:

// Event definitions with their possible branches
~event validate { value: i32 }
| valid { result: i32 }
| invalid { msg: []const u8 }

~event double { value: i32 }
| ok { result: i32 }

~event success { result: i32 }
| done {}

// Implementations
~proc validate {
    if (value > 0) {
        return .{ .valid = .{ .result = value } };
    }
    return .{ .invalid = .{ .msg = "Value must be positive" } };
}

~proc double {
    return .{ .ok = .{ .result = value * 2 } };
}

~proc success {
    std.debug.print("Success: {}\n", .{result});
    return .{ .done = .{} };
}

// The flow - event continuations in action
~validate(value: 42)
| valid v |> double(value: v.result)
    | ok doubled |> success(result: doubled.result)
        | done |> _
| invalid e |> failure(msg: e.msg)
    | done |> _

What does this compile to? Zig code with tagged unions and switch statements:

// Events become typed structs
pub const validate_event = struct {
    pub const Input = struct { value: i32 };
    pub const Output = union(enum) {
        valid: struct { result: i32 },
        invalid: struct { msg: []const u8 },
    };
    pub fn handler(input: Input) Output {
        if (input.value > 0) {
            return .{ .valid = .{ .result = input.value } };
        }
        return .{ .invalid = .{ .msg = "Value must be positive" } };
    }
};

// The flow becomes a function with exhaustive switches
pub fn flow0() void {
    const result_0 = validate_event.handler(.{ .value = 42 });
    switch (result_0) {
        .valid => |v| {
            const result_1 = double_event.handler(.{ .value = v.result });
            const doubled = result_1.ok;
            const result_2 = success_event.handler(.{ .result = doubled.result });
            _ = result_2;
        },
        .invalid => |e| {
            const result_1 = failure_event.handler(.{ .msg = e.msg });
            _ = result_1;
        },
    }
}

Look at what happened:

  1. Events → Tagged Unions: Each | branch becomes a variant in a union(enum)
  2. Branches → Exhaustive Switches: The continuation tree becomes switch arms
  3. Flows → Direct Function Calls: No runtime dispatch, no queues, no event loop

The event continuation has completely disappeared. What remains is plain, efficient, type-safe Zig code.

This is the core insight: Event continuations are a zero-cost abstraction. They give you a beautiful way to express branching control flow, and then they compile away entirely. No runtime overhead. No dynamic dispatch. Just function calls and switches.

The Zig compiler can then optimize this further - inlining handlers, eliminating dead branches, proving exhaustiveness at compile time.

The Monadic Structure

If you squint, you’ll recognize what this is: a free monad that compiles away.

Each event invocation is a monadic bind. The branch determines which continuation runs. The payload carries context forward. The |> arrow is >>= in disguise:

~validate(value)           -- invoke computation
| valid v |>               -- bind result, pattern match on sum type
    double(v.result)       -- chain into next computation

This is Result<T, E> or Either with explicit continuation syntax. But unlike Haskell’s monads or Rust’s ? operator, the abstraction doesn’t just feel zero-cost - it literally disappears. No trait objects. No vtables. No monomorphization overhead. The compiler sees through the entire continuation tree and emits direct control flow.

The algebraic structure:

  • Branches form a sum type: | valid | invalidunion(enum) { valid, invalid }
  • Payloads form product types: { result: i32 }struct { result: i32 }
  • Continuations form a tree: Nested |> → Nested switches

You get compositional semantics with zero runtime representation. The monad is a compile-time fiction that guides code generation.

This is the event continuation as computational monad in the Leibniz sense: the indivisible unit from which programs are composed. Not a library abstraction you pay for - a language primitive that exists only to instruct the compiler.

Branches Without Judgment

There’s something else hiding in the syntax. Compare:

Rust’s Result:

match file.read() {
    Ok(data) => process(data),      // The "happy" path
    Err(e) => handle_error(e),      // The "sad" path
}

Exceptions:

try {
    process(file.read());           // The "normal" path
} catch (Exception e) {             // The "exceptional" path
    handleError(e);
}

Koru:

~file:read(path)
| success data |> process(data)
| not_found |> create_default(path)
| permission_denied |> request_elevation()

See the difference?

Result<T, E> encodes a value judgment: Ok is good, Err is bad. One path is “happy,” one is “sad.” Even Either<L, R> has the convention that “Right is right.”

Exceptions are worse: there’s the “normal” flow and the “exceptional” flow. The exception path is literally designed to be ignored - you can always add another catch clause, or catch Exception and swallow everything.

Koru’s branches are morally neutral.

not_found isn’t an error - it’s an outcome. Maybe you create the file. permission_denied isn’t a failure - it’s information. Maybe you request elevation and retry. The branch names come from your domain, not from a generic success/failure binary.

And because they compile to exhaustive switches, you can’t ignore any of them. There’s no catch (Exception e) escape hatch. Every branch is a first-class citizen that demands explicit handling.

This matters because “happy path” thinking is a cognitive trap. Real systems have multiple valid outcomes, not just “worked” and “didn’t work.” Event continuations force you to model that reality.

But Where Did This Come From?

We did digital archaeology on our own codebase. We went looking for the exact moment event continuations were born. And we found it - not just one moment, but an entire sleepless night where two parallel concepts emerged.

The Night of August 27, 2025

1:18 AM - The Vision

In beist-core, a commit landed:

6f45470 docs: add VISION.md outlining BEIST's evolution into an autonomous development platform

Deep in that document, a seemingly small detail:

“Production: Zig with ring buffers”

The idea that the event queue itself could be swapped out. Development in TypeScript, production in Zig. And the transport mechanism? Ring buffers.

2:42 AM - The Chains

84 minutes later, another commit:

bec355b docs: add S-expression workflow language spec and examples

Inside was ROADMAP.md. And in that file, this:

BEIST> chain (if (tests:pass)
                 (deploy:prod)
                 (rollback))
BEIST> chain (parallel
               (gpu:compile)
               (cpu:compile)
               (tests:run))
             (join zig:link)

There it is. Chains. Branches. Parallel execution. S-expression syntax describing event flow.

Two concepts, born 84 minutes apart:

  1. Ring buffers - the transport mechanism
  2. Continuation chains - the control flow

The Parallel Evolution

Once we had those timestamps, we could trace how both concepts evolved in parallel:

August 31, 2025 - The Ring Buffer Manifests

Four days later, beist-os appeared. In its ARCHITECTURE.md:

pub const EventQueue = struct {
    buffer: []?Event,        // Pre-allocated ring buffer
    head: Atomic(usize),     // Consumer position
    tail: Atomic(usize),     // Producer position
    capacity: usize,         // Power of 2 for fast modulo
};

Performance: 60M+ events/second. Lock-free. The vision from 1:18 AM made real.

September 2, 2025 - Continuations Become Data

The chain concept evolved. A new commit in beist-core:

71777a3 feat: add support for event continuations with nested data and chaining

Events started carrying their own “what happens next”:

beist emit build:start --data '{
  "then": "deploy:start"
}'

beist emit setup:complete --data '{
  "then": ["compile:core", "compile:ui", "test:all"]
}'

The then field. Events knowing their own destiny. Continuations as data.

September 10-14, 2025 - The Marriage

The two concepts merged in beist-rings. Events would travel through ring buffers, carrying their continuation trees with them:

const FileReadEvent = struct {
    pub fn success(self: FileReadEvent, data: []const u8) void { ... }
    pub fn file_not_found(self: FileReadEvent, path: []const u8) void { ... }
    pub fn permission_denied(self: FileReadEvent, user: []const u8) void { ... }
};

const workflow = parseSexpr("
    (file:read
        (success file:process (ok file:save))
        (file_not_found file:create (ok file:read))
        (permission_denied security:alert))
");

Event types with compile-time defined continuation methods. S-expressions describing routing. Bloom filters for O(1) routing decisions. The architecture was complete.

September 16, 2025 - Koru

Two days later, a new repository. The S-expression syntax became something cleaner:

~file:read(path)
| success data |> process(data)
| not_found |> create(path)

The pipe. The branch name. The arrow. The continuation.

20 days from sleepless night to programming language.

The Timeline

DateTimeCommitWhat Was Born
Aug 27, 20251:18 AMVISION.md“Zig with ring buffers”
Aug 27, 20252:42 AMROADMAP.mdS-expr chains with branching
Aug 31, 202510:32 PMbeist-osLock-free ring buffer (60M events/sec)
Sep 2, 20251:56 AMbeist-corethen field - events carry continuations
Sep 10, 202510:39 PMbeist-ringsBloom-routed ring buffers
Sep 14, 20255:55 PMEVENT_CONTINUATIONS.mdFull architecture spec
Sep 16, 20255:46 PMKoru initialLanguage begins
Sep 17, 20257:44 PMHello WorldIt compiles

Why Two Concepts?

What we found wasn’t just the birth of continuations. It was the birth of a complete event architecture:

The Transport (Rings):

  • Lock-free ring buffers
  • Pre-allocated memory
  • Bloom filter routing
  • Zero-copy where possible
  • 177M messages/second

The Control Flow (Continuations):

  • Events define their branches at compile time
  • Routing described in S-expressions (later: Koru syntax)
  • Trees carried in event payloads
  • Cursor-based traversal

One without the other is incomplete. Ring buffers without continuations are just fast queues. Continuations without ring buffers are just slow trees.

Together, they’re a new way to think about event-driven systems.

The Artifacts

The original documents from that night are archived here:

These aren’t polished design documents. They’re rough sketches from 2 AM, written by someone who couldn’t sleep because an idea wouldn’t let go.

That’s how languages get born.

What This Enables

Because event continuations compile to exhaustive switches on tagged unions, you get:

  • Compile-time branch verification: Miss a branch? Compiler error.
  • Zero runtime overhead: No event loop, no dispatch tables, no allocations.
  • Fearless refactoring: Add a branch to an event, and every flow that uses it must handle it.

The continuation tree is a compile-time contract. The runtime cost is zero.


What’s Next?

Event continuations as a language feature are just the beginning. The same primitive - branches, payloads, and explicit next-steps - has implications beyond local control flow:

  • Phantom obligations: Because the compiler sees all branches exhaustively, it can track what you MUST handle before scope exit. Auto-dispose, compile-time resource safety, GC ergonomics without GC runtime - all downstream from event continuations.
  • Wire protocols: What if HTTP responses carried continuation trees instead of data to interpret?
  • An operating system: We’re building a layer where event continuations are the computational primitive.

For now, the core insight stands:

Event continuations compile away completely. They’re a beautiful abstraction for the programmer that becomes zero-cost at runtime.


The Commits Don’t Lie

commit bec355b
Author: [Anonymous]
Date:   Wed Aug 27 02:42:08 2025 +0200

    docs: add S-expression workflow language spec and examples

That’s the birth certificate. August 27, 2025, 2:42 AM, Central European Summer Time.

Fourteen months earlier, the same person wanted to make a fun game in Godot. The game still doesn’t exist. But the programming language does.


This post was created through digital archaeology - searching git histories across beist-core, beist-os, beist-rings, beist-zig, and koru to find the exact moments of inception. All timestamps are from actual commits. The evolution is documented in code.