diff --git a/fern/01-guide/05-baml-advanced/runtime-events.mdx b/fern/01-guide/05-baml-advanced/runtime-events.mdx
new file mode 100644
index 0000000000..77d9c9bfac
--- /dev/null
+++ b/fern/01-guide/05-baml-advanced/runtime-events.mdx
@@ -0,0 +1,1448 @@
+---
+title: Runtime Events
+---
+
+
+This feature was added in 0.210.0
+
+
+When running multi-step workflows, you need to be able to get information about
+the running workflow. You might need this information to show incremental
+results to your app’s users, or to debug a complex workflow combining multiple
+LLM calls.
+
+BAML makes this possible though an event system that connects variables in your
+BAML Workflow code to the Python/TypeScript/etc client code that you used to
+invoke the workflow.
+
+## Using Markdown blocks to track execution
+
+Markdown Blocks are automatically tracked when you run BAML
+workflows, and your client code can track which block is currently executing. In
+the following example, your client can directly use the markdown headers to
+render the current status on a status page:
+
+```baml BAML
+struct Post {
+ title string
+ content string
+}
+
+// Browse a URL and produce a number of posts describing
+// its what was found there for our marketing site.
+function MakePosts(source_url: string, count: int) -> Post[] {
+ # Summarize Source
+ let source = LLMSummarizeSource(source_url);
+
+ # Determine Topic
+ let topic = LLMInferTopic(source);
+
+ # Generate Marketing Post Ideas
+ let ideas: string[] = LLMIdeas(topic, source);
+
+ # Generate posts
+ let posts: Post[] = [];
+ for (idea in ideas) {
+
+ ## Create the post
+ let post = LLMGeneratePost(idea, source);
+
+ ## Quality control
+ let quality = LLMJudgePost(post, idea, source);
+ if (quality > 8) {
+ posts.push(post);
+ }
+ }
+}
+```
+
+## Track Emitted Block Events
+
+You can track emitted block events from your client code.
+
+When you generate client code from your BAML code, we produce listener structs
+that allow you to hook in to events.
+
+
+
+
+In your client code, you can bind events to callbacks:
+
+```python Python
+ # baml_client/events.py
+from typing import TypeVar, Generic, Callable, Union
+
+class BlockEvent:
+ """
+ Event fired when entering or exiting a markdown block
+ """
+ block_label: str
+ event_type: str # "enter" | "exit"
+
+class MakePostsEventCollector:
+ """Event collector for MakePosts function"""
+
+ def on_block(self, handler: Callable[[BlockEvent], None]) -> None:
+ """Register a handler for block events"""
+ pass
+```
+
+```python Python
+ # app.py
+ from baml_client.sync_client import { b }
+ from baml_client.types import Event
+ import baml_client.events
+
+ def Example():
+ # Get an Events callback collector with the right type
+ # for your MakePosts() function.
+ ev = events.MakePostsCollector()
+
+ # Associate the block event with your own callback.
+ events.on_block(lambda ev: print(ev.block_label))
+
+ # Invoke the function.
+ posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"events": ev})
+ print(posts)
+```
+
+
+
+In your client code, you can bind events to callbacks:
+
+```typescript
+// baml_client/event.ts
+export interface BlockEvent {
+ block_label: string;
+ event_type: "enter" | "exit";
+}
+
+export interface MakePostsEventCollector {
+ // Register a handler for block events.
+ on_block(handler: (ev: BlockEvent) => void): void;
+}
+```
+
+```typescript
+ // index.ts
+ import { b, events } from "./baml-client"
+ import type { Event } from "./baml-client/types"
+
+ async function Example() {
+ // Get an Events callback collector with the right type
+ // for your MakePosts() function.
+ let ev = events.MakePosts()
+
+ // Associate the block event with your own callback.
+ events.on_block((ev) => {
+ console.log(ev.block_label)
+ });
+
+ // Invoke the function.
+ const posts = await b.MakePosts(
+ "https://wikipedia.org/wiki/DNA",
+ {"events": ev}
+ )
+ console.log(posts)
+ }
+```
+
+
+
+In your client code, you can consume events via channels:
+
+```go
+// baml_client/events.go
+package events
+
+type BlockEvent struct {
+ BlockLabel string `json:"block_label"`
+ EventType string `json:"event_type"` // "enter" | "exit"
+}
+
+type MakePostsEventCollector struct {
+ blockEvents chan BlockEvent
+ // ... additional event channels are initialized elsewhere.
+}
+
+func NewMakePostsEventCollector() *MakePostsEventCollector {
+ return &MakePostsEventCollector{
+ blockEvents: make(chan BlockEvent, 100),
+ }
+}
+
+// BlockEvents provides block execution updates as a channel.
+func (c *MakePostsEventCollector) BlockEvents() <-chan BlockEvent {
+ return c.blockEvents
+}
+```
+
+```go
+// main.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ b "example.com/myproject/baml_client"
+ "example.com/myproject/baml_client/events"
+)
+
+func main() {
+ ctx := context.Background()
+
+ // Get an event collector with the right channels
+ // for your MakePosts() function.
+ ev := events.NewMakePostsEventCollector()
+
+ // Consume block events asynchronously so updates are printed as they arrive.
+ go func() {
+ for blockEvent := range ev.BlockEvents() {
+ fmt.Println(blockEvent.BlockLabel)
+ }
+ }()
+
+ // Invoke the function.
+ posts, err := b.MakePosts(ctx, "https://wikipedia.org/wiki/DNA", &b.MakePostsOptions{
+ Events: ev,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%+v\n", posts)
+}
+```
+
+
+
+## Track variables with `emit`
+
+Variable updates can also be tracked with events. To mark an update for tracking,
+attach `@emit` to the variable declaration (or update its options) so the runtime knows to emit changes.
+
+```baml BAML
+let foo = State { counter: 0 } @emit;
+foo.counter += 1; // *** This triggers an event
+```
+
+
+ ```python
+ events.on("foo", lambda st: my_state_update_handler(st))
+ ```
+
+ ```typescript
+ events.on("foo", (st) => my_state_update_handler(st))
+ ```
+
+ ```go
+ // Consume events from the events.on_foo channel
+ for st := range events.FooEvents {
+ handleState(st)
+ }
+ ```
+
+
+Updates can be tracked automatically or manually, depending on the `@emit` options you choose.
+Automatic tracking will emit events any time a variable is updated. Manual tracking
+only emits events after updates that you specify.
+
+### Auto Tracking
+
+
+
+Let’s see how we would use this capability to automatically track the progress of our
+marketing post
+generation workflow:
+
+```baml BAML
+function MakePosts(source_url: string) -> Post[] {
+ let source = LLMSummarizeSource(source_url);
+ let topic = LLMInferTopic(source);
+ let ideas: string[] = LLMIdeas(topic, source);
+ let posts_target_length = ideas.len();
+
+ let progress_percent: int = 0 @emit; // *** Emit marker used here.
+
+ let posts: Post[] = [];
+ for ((i,idea) in ideas.enumerate()) {
+ let post = LLMGeneratePost(idea, source);
+ let quality = LLMJudgePost(post, idea, source);
+ if (quality > 8) {
+ posts.push(post);
+ } else {
+ posts_target_length -= 1;
+ }
+ // *** This update will trigger events visible to the client.
+ progress_percent = i * 100 / posts_target_length
+ }
+}
+```
+
+### Emit parameters
+
+The variable tracking can be controled in several ways.
+
+ - `@emit(when=MyFilterFunc)` - Only emits when `MyFilterFunc` returns `true`
+ - `@emit(when=false)` - Never auto emit (only emit when manually triggered)
+ - `@emit(skip_def=true)` - Emits every time the variable is updated, but not on initialization
+ - `@emit(name=my_channel)` - Emits events on a channel you spceify (default is the variable name)
+
+The filter functions you pass to `when` should take two parameters. It will be called every
+time an value is updated. The first parameter is the previous version of the value, and the
+second is the new version. With these two parameters, you can determine whether the event should
+be emitted or not (often by comparing the current to the previous, for deduplication).
+
+If you do not specify a filter function, BAML deduplicates automatically emitted events for you.
+You could replicate the same behavior by using `@emit(when=MyFilterFunc)` where `MyFilterFunc`
+is defined as:
+
+```baml BAML
+function MyFilterFunc(prev: MyObject, curr: MyObject) -> bool {
+ !(prev.id() == curr.id()) || !(baml.deep_eq(prev, curr))
+}
+```
+
+### Manual Tracking
+
+Sometimes you want no automatic tracking at all. For example, if you are building up a complex
+value in multiple steps, you may not want your application to see that value while it is still
+under construction. In that case, use `@emit(when=false)` when initializing the variable, and
+call `.$emit()` on the variable when you want to manually trigger an event.
+
+
+```baml BAML
+function ConstructValue(description: string) -> Character {
+ let character = Character { name: "", age: 0, skills: [] } @emit(when=false);
+ character.name = LLMChooseName(description);
+ character.age = LLMChooseAge(description);
+ character.skills = LLMChooseSkills(description);
+ character.$emit() // *** Only emit when done building the character.
+}
+```
+
+### Sharing a Channel
+
+Sometimes you want multiple variables to send update events on the same channel, for example,
+if you want a single view of all the state updates from multiple values in your BAML code,
+because you will render them into a single view in the order that they are emitted.
+
+```baml BAML
+function DoWork() -> bool {
+ let status = "Starting" @emit(name=updates);
+ let progress = 0 @emit(name=updates, skip_def=true);
+ for (let i = 0; i < 100; i++) {
+ progress = i; // *** These updates will apear on the `updates` channel.
+ }
+ status = "Done";
+ return true;
+}
+```
+
+## Receiving Events from Client Code
+
+
+
+
+
+When you generate a BAML client for our original function, your Python SDK will
+include a `MakePostsEventCollector` class. This class contains configurable callbacks
+for all your tracked variables. For example, it contains callbacks for `progress_percent`
+because we marked that variable with `@emit`. The callbacks will receive an `int` data payload,
+because `progress_percent` is an `int`.
+
+```python
+# baml_client/events.py
+
+T = TypeVar('T')
+
+class VarEvent(Generic[T]):
+ """
+ Event fired when an emitted variable is updated
+ """
+ value: T
+ timestamp: str
+ function_name: str
+
+class MakePostsVarsCollector:
+ progress_percent: Callable[[VarEvent[int]], None]
+
+class MakePostsEventCollector:
+ """Event collector for MakePosts function"""
+ vars: MakePostsVarsCollector
+```
+
+```python
+# app.py
+from baml_client.sync_client import { b }
+from baml_client.types import Event
+import baml_client.events
+
+def Example():
+ # Get an Events callback collector with the right type
+ # for your MakePosts() function.
+ ev = events.MakePosts()
+
+ # Track the progress_percent variable updates
+ events.vars.on_progress_percent(lambda percent: print(f"Progress: {percent}%"))
+
+ # Invoke the function.
+ posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"events": ev})
+ print(posts)
+```
+
+
+
+
+When you generate a BAML client for this function, its `MakePostsEventCollector`
+will accept callbacks for `progress_percent` because we marked that variable with
+`@emit`, and the callbacks will receive an `int` data payload, because
+`progress_percent` is an `int`.
+
+```typescript
+// baml_client/events.ts
+import { VarEvent } from "./types"
+
+export interface MakePostsEventCollector {
+ on_var_progress_percent(callback: (percent: number) => void): void
+}
+
+export function MakePosts(): MakePostsEventCollector {
+ return {
+ on_var_progress_percent(callback: (percent: number) => void): void {
+ // Implementation details
+ }
+ }
+}
+```
+
+```typescript
+// index.ts
+import { b, events } from "./baml-client"
+import type { VarEvent } from "./baml-client/types"
+
+async function Example() {
+ // Get an Events callback collector with the right type
+ // for your MakePosts() function.
+ let ev = events.MakePosts()
+
+ // Track the progress_percent variable updates
+ events.on_progress_percent((percent) => {
+ console.log(`Progress: ${percent}%`)
+ });
+
+ // Invoke the function.
+ const posts = await b.MakePosts(
+ "https://wikipedia.org/wiki/DNA",
+ {"events": ev }
+ )
+ console.log(posts)
+}
+```
+
+
+
+
+In your client code, you can track these emitted variables by constructing the
+generated event collector and reading from the channels it exposes.
+
+```go
+// baml_client/events.go
+package events
+
+import "time"
+
+type BlockEvent struct {
+ BlockLabel string `json:"block_label"`
+ EventType string `json:"event_type"` // "enter" | "exit"
+ Timestamp time.Time `json:"timestamp"`
+}
+
+type VarEvent[T any] struct {
+ VariableName string `json:"variable_name"`
+ Value T `json:"value"`
+ Timestamp time.Time `json:"timestamp"`
+ FunctionName string `json:"function_name"`
+}
+
+type MakePostsEventCollector struct {
+ blockEvents chan BlockEvent
+ progressPercentEvents chan VarEvent[int]
+}
+
+func NewMakePostsEventCollector() *MakePostsEventCollector {
+ return &MakePostsEventCollector{
+ blockEvents: make(chan BlockEvent, 100),
+ progressPercentEvents: make(chan VarEvent[int], 100),
+ }
+}
+
+// BlockEvents returns block execution updates.
+func (c *MakePostsEventCollector) BlockEvents() <-chan BlockEvent {
+ return c.blockEvents
+}
+
+// ProgressPercentEvents streams progress_percent variable updates.
+func (c *MakePostsEventCollector) ProgressPercentEvents() <-chan VarEvent[int] {
+ return c.progressPercentEvents
+}
+```
+
+```go
+// main.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ b "example.com/myproject/baml_client"
+ "example.com/myproject/baml_client/events"
+)
+
+func main() {
+ ctx := context.Background()
+
+ // Get an event collector with the right channels
+ // for your MakePosts() function.
+ ev := events.NewMakePostsEventCollector()
+
+ // Consume block events and progress updates concurrently.
+ go func() {
+ for block := range ev.BlockEvents() {
+ fmt.Printf("Block: %s\n", block.BlockLabel)
+ }
+ }()
+
+ go func() {
+ for percent := range ev.ProgressPercentEvents() {
+ fmt.Printf("Progress: %d%%\n", percent.Value)
+ }
+ }()
+
+ // Invoke the function.
+ posts, err := b.MakePosts(ctx, "https://wikipedia.org/wiki/DNA", &b.MakePostsOptions{
+ Events: ev,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%+v\n", posts)
+}
+```
+
+
+
+For details about the types of events, see [BAML Language Reference](/ref/baml_client/events)
+
+# Streaming
+
+If updates to variables tagged with `@emit` include large amounts of data that you want to start
+surfacing to your application before they are done being generated, you want to use
+the streaming event interface. Streaming events are available for all `@emit` variables,
+but they are generally only useful when assigning a variable from the result of
+an LLM function. All other streamed events will return their values in a single
+complete chunk.
+
+```baml BAML
+function DescribeTerminatorMovies() -> string[] {
+ let results = [];
+ for (x in [1,2,3]) {
+ let movie_text = LLMElaborateOnTopic("Terminator " + std.to_string(x)) @emit;
+ results.push(movie_text);
+ }
+ return results;
+}
+
+function LLMElaborateOnTopic(topic: string) -> string {
+ client GPT4
+ prompt #"
+ Write a detailed 500-word analysis of {{ topic }}.
+ Include plot summary, themes, and cultural impact.
+ "#
+}
+```
+
+This function will take a while to run because it calls an LLM function
+three times. However, you can stream the results of each of these calls
+to start getting immediate feedback from the workflow as the LLM generates
+text token by token.
+
+The streaming listeners are available in client code under a separate streaming
+module that mirrors the structure of the regular event collectors.
+
+
+
+
+```python
+# baml_client/events.py
+from typing import TypeVar, Generic, Callable
+from baml_client.types import BamlStream, VarEvent
+
+T = TypeVar('T')
+
+class VarEvent(Generic[T]):
+ """
+ Event fired when an emitted variable is updated
+ """
+ variable_name: str
+ value: T
+ timestamp: str
+ function_name: str
+
+class BlockEvent:
+ """
+ Event fired when entering or exiting a markdown block
+ """
+ block_label: str
+ event_type: str # "enter" | "exit"
+
+class MakePostsVarsCollector:
+ progress_percent: Callable[[VarEvent[int]], None]
+
+class DescribeTerminatorMoviesEventCollector:
+ """Event collector for DescribeTerminatorMovies function with both regular and streaming events"""
+
+ def on_block(self, handler: Callable[[BlockEvent], None]) -> None:
+ """Register a handler for block events"""
+ pass
+
+ def on_var_movie_text(self, handler: Callable[[VarEvent[str]], None]) -> None:
+ """Register a handler for movie_text variable updates"""
+ pass
+
+ def on_stream_movie_text(self, handler: Callable[[BamlStream[VarEvent[str]]], None]) -> None:
+ """Register a handler for streaming movie_text variable updates"""
+ pass
+```
+
+```python
+# app.py
+from baml_client.sync_client import b
+import baml_client.events as events
+
+def example():
+ # Create the unified event collector
+ ev = events.DescribeTerminatorMoviesEventCollector()
+
+ # Track streaming updates for the main emitted variable
+ def handle_movie_text_stream(stream):
+ for event in stream:
+ print(f"Streaming movie text: {event.value}")
+
+ ev.on_stream_movie_text(handle_movie_text_stream)
+
+ # Invoke the function with events
+ results = b.DescribeTerminatorMovies({"events": ev})
+ print("Final results:", results)
+```
+
+
+
+
+```typescript
+// baml_client/events.ts
+import { BamlStream, VarEvent } from "./types";
+
+export interface BlockEvent {
+ block_label: string;
+ event_type: "enter" | "exit";
+}
+
+export interface VarEvent {
+ variable_name: string;
+ value: T;
+ timestamp: string;
+ function_name: string;
+}
+
+export interface DescribeTerminatorMoviesEventCollector {
+ // Regular event handlers
+ on_block(handler: (ev: BlockEvent) => void): void;
+ on_var_movie_text(handler: (ev: VarEvent) => void): void;
+
+ // Streaming event handlers
+ on_stream_movie_text(handler: (stream: BamlStream>) => void): void;
+}
+
+export function DescribeTerminatorMovies(): DescribeTerminatorMoviesEventCollector {
+ return {
+ on_block(handler: (ev: BlockEvent) => void): void {
+ // Implementation details
+ },
+ on_var_movie_text(handler: (ev: VarEvent) => void): void {
+ // Implementation details
+ },
+ on_stream_movie_text(handler: (stream: BamlStream>) => void): void {
+ // Implementation details
+ }
+ }
+}
+```
+
+```typescript
+// index.ts
+import { b, events } from "./baml-client"
+
+async function example() {
+ // Create the unified event collector
+ let ev = events.DescribeTerminatorMovies()
+
+ // Track streaming updates for the main emitted variable
+ ev.on_stream_movie_text(async (stream) => {
+ for await (const event of stream) {
+ console.log(`Streaming movie text: ${event.value}`)
+ }
+ })
+
+ // Invoke the function with events
+ const results = await b.DescribeTerminatorMovies({"events": ev})
+ console.log("Final results:", results)
+}
+```
+
+
+
+
+```go
+// baml_client/events.go
+package events
+
+import "time"
+
+type BlockEvent struct {
+ BlockLabel string `json:"block_label"`
+ EventType string `json:"event_type"` // "enter" | "exit"
+ Timestamp time.Time `json:"timestamp"`
+}
+
+type VarEvent[T any] struct {
+ VariableName string `json:"variable_name"`
+ Value T `json:"value"`
+ Timestamp time.Time `json:"timestamp"`
+ FunctionName string `json:"function_name"`
+}
+
+type DescribeTerminatorMoviesEventCollector struct {
+ blockEvents chan BlockEvent
+ movieTextEvents chan VarEvent[string]
+ movieTextStreams chan (<-chan VarEvent[string])
+}
+
+func NewDescribeTerminatorMoviesEventCollector() *DescribeTerminatorMoviesEventCollector {
+ return &DescribeTerminatorMoviesEventCollector{
+ blockEvents: make(chan BlockEvent, 100),
+ movieTextEvents: make(chan VarEvent[string], 100),
+ movieTextStreams: make(chan (<-chan VarEvent[string]), 10),
+ }
+}
+
+func (c *DescribeTerminatorMoviesEventCollector) BlockEvents() <-chan BlockEvent {
+ return c.blockEvents
+}
+
+func (c *DescribeTerminatorMoviesEventCollector) MovieTextEvents() <-chan VarEvent[string] {
+ return c.movieTextEvents
+}
+
+// MovieTextStreams produces a stream-of-streams for emitted movie_text updates.
+func (c *DescribeTerminatorMoviesEventCollector) MovieTextStreams() <-chan (<-chan VarEvent[string]) {
+ return c.movieTextStreams
+}
+```
+
+```go
+// main.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ b "example.com/myproject/baml_client"
+ "example.com/myproject/baml_client/events"
+)
+
+func main() {
+ ctx := context.Background()
+
+ // Create the unified event collector
+ ev := events.NewDescribeTerminatorMoviesEventCollector()
+
+ // Track block events and single-value updates concurrently.
+ go func() {
+ for block := range ev.BlockEvents() {
+ fmt.Printf("Block: %s\n", block.BlockLabel)
+ }
+ }()
+
+ go func() {
+ for movieText := range ev.MovieTextEvents() {
+ fmt.Printf("Variable movie text: %s\n", movieText.Value)
+ }
+ }()
+
+ // Track streaming updates using the channel-of-channels pattern.
+ go func() {
+ for stream := range ev.MovieTextStreams() {
+ go func(inner <-chan events.VarEvent[string]) {
+ for event := range inner {
+ fmt.Printf("Streaming movie text: %s\n", event.Value)
+ }
+ }(stream)
+ }
+ }()
+
+ // Invoke the function with events
+ results, err := b.DescribeTerminatorMovies(ctx, &b.DescribeTerminatorMoviesOptions{
+ Events: ev,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("Final results: %+v\n", results)
+}
+```
+
+
+
+## Combining Regular Events and Streaming
+
+You can use both regular events and streaming events together in a single unified collector to get comprehensive observability:
+
+
+
+
+```python
+from baml_client.sync_client import b
+import baml_client.events as events
+
+def comprehensive_example():
+ # Create unified event collector
+ ev = events.DescribeTerminatorMoviesEventCollector()
+
+ # Regular events for workflow progress
+ ev.on_block(lambda block: print(f"Block: {block.block_label}"))
+ ev.on_var_movie_text(lambda movie_text: print(f"Variable movie text: {movie_text.value}"))
+
+ # Streaming events for real-time content
+ def handle_stream(stream):
+ for event in stream:
+ print(f"Streaming content: {event.value}")
+
+ ev.on_stream_movie_text(handle_stream)
+
+ # Use single events parameter
+ results = b.DescribeTerminatorMovies({"events": ev})
+```
+
+
+
+
+```typescript
+import { b, events } from "./baml-client"
+
+async function comprehensiveExample() {
+ // Create unified event collector
+ let ev = events.DescribeTerminatorMovies()
+
+ // Regular events for workflow progress
+ ev.on_block((block) => console.log(`Block: ${block.block_label}`))
+ ev.on_var_movie_text((movieText) => console.log(`Variable movie text: ${movieText.value}`))
+
+ // Streaming events for real-time content
+ ev.on_stream_movie_text(async (stream) => {
+ for await (const event of stream) {
+ console.log(`Streaming content: ${event.value}`)
+ }
+ })
+
+ // Use single events parameter
+ const results = await b.DescribeTerminatorMovies({ events: ev })
+}
+```
+
+
+
+
+```go
+func comprehensiveExample() {
+ ctx := context.Background()
+
+ // Create unified event collector
+ ev := events.NewDescribeTerminatorMoviesEventCollector()
+
+ // Regular events for workflow progress
+ go func() {
+ for block := range ev.BlockEvents() {
+ fmt.Printf("Block: %s\n", block.BlockLabel)
+ }
+ }()
+ go func() {
+ for movieText := range ev.MovieTextEvents() {
+ fmt.Printf("Variable movie text: %s\n", movieText.Value)
+ }
+ }()
+
+ // Streaming events for real-time content
+ go func() {
+ for stream := range ev.MovieTextStreams() {
+ go func(inner <-chan events.VarEvent[string]) {
+ for event := range inner {
+ fmt.Printf("Streaming content: %s\n", event.Value)
+ }
+ }(stream)
+ }
+ }()
+
+ // Use single Events parameter
+ results, err := b.DescribeTerminatorMovies(ctx, &b.DescribeTerminatorMoviesOptions{
+ Events: ev,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+```
+
+
+
+# Usage Scenarios
+
+## Track events from subfunctions
+
+When your main workflow calls other BAML functions, you can track events from those subfunctions as well. If `MakePosts()` calls `Foo()`, and `Foo()` contains variables tagged with `@emit` or markdown blocks, the client invoking `MakePosts()` can subscribe to those subfunction events through dedicated records in the `EventCollector`.
+
+Consider this example where `MakePosts()` calls a helper function:
+
+```baml BAML
+function MakePosts(source_url: string) -> Post[] {
+ let posts = GeneratePostsWithProgress(source_url);
+ return posts;
+}
+
+function GeneratePostsWithProgress(url: string) -> Post[] {
+ # Analyzing content
+ let content = LLMAnalyzeContent(url);
+
+ let progress_status = "Starting generation" @emit;
+
+ # Generate posts
+ let posts = [];
+ for (i in [1,2,3]) {
+ progress_status = "Generating post " + i.to_string();
+ posts.push(LLMGeneratePost(content, i));
+ }
+
+ return posts;
+}
+```
+
+
+
+
+```python
+# baml_client/events.py
+
+class GeneratePostsWithProgressEventCollector:
+ """Event collector for GeneratePostsWithProgress function"""
+
+ def on_block(self, handler: Callable[[BlockEvent], None]) -> None:
+ """Register a handler for block events from this function"""
+ pass
+
+ def on_var_progress_status(self, handler: Callable[[VarEvent[str]], None]) -> None:
+ """Register a handler for progress_status variable updates"""
+ pass
+
+class MakePostsEventCollector:
+ """Event collector for MakePosts function"""
+
+ def __init__(self):
+ self.function_GeneratePostsWithProgress = GeneratePostsWithProgressEventCollector()
+```
+
+```python
+# app.py
+from baml_client.sync_client import b
+import baml_client.events as events
+
+def example():
+ # Create the main event collector
+ ev = events.MakePostsEventCollector()
+
+ # Subscribe to subfunction events
+ ev.function_GeneratePostsWithProgress.on_var_progress_status(
+ lambda e: print(f"Subfunction progress: {e.value}")
+ )
+
+ ev.function_GeneratePostsWithProgress.on_block(
+ lambda e: print(f"Subfunction block: {e.block_label}")
+ )
+
+ # Invoke the function
+ posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"events": ev})
+ print(posts)
+```
+
+
+
+
+```typescript
+// baml_client/events.ts
+
+export interface GeneratePostsWithProgressEventCollector {
+ on_block(handler: (ev: BlockEvent) => void): void;
+ on_var_progress_status(handler: (ev: VarEvent) => void): void;
+}
+
+export interface MakePostsEventCollector {
+ function_GeneratePostsWithProgress: GeneratePostsWithProgressEventCollector;
+}
+
+export function MakePosts(): MakePostsEventCollector {
+ return {
+ function_GeneratePostsWithProgress: {
+ on_block(handler: (ev: BlockEvent) => void): void {
+ // Implementation details
+ },
+ on_var_progress_status(handler: (ev: VarEvent) => void): void {
+ // Implementation details
+ }
+ }
+ }
+}
+```
+
+```typescript
+// index.ts
+import { b, events } from "./baml-client"
+
+async function example() {
+ // Create the main event collector
+ let ev = events.MakePosts()
+
+ // Subscribe to subfunction events
+ ev.function_GeneratePostsWithProgress.on_var_progress_status((e) => {
+ console.log(`Subfunction progress: ${e.value}`)
+ })
+
+ ev.function_GeneratePostsWithProgress.on_block((e) => {
+ console.log(`Subfunction block: ${e.block_label}`)
+ })
+
+ // Invoke the function
+ const posts = await b.MakePosts("https://wikipedia.org/wiki/DNA", {"events": ev})
+ console.log(posts)
+}
+```
+
+
+
+
+```go
+// baml_client/events.go
+package events
+
+import "time"
+
+type BlockEvent struct {
+ BlockLabel string `json:"block_label"`
+ EventType string `json:"event_type"`
+ Timestamp time.Time `json:"timestamp"`
+}
+
+type VarEvent[T any] struct {
+ VariableName string `json:"variable_name"`
+ Value T `json:"value"`
+ Timestamp time.Time `json:"timestamp"`
+ FunctionName string `json:"function_name"`
+}
+
+type GeneratePostsWithProgressEventCollector struct {
+ blockEvents chan BlockEvent
+ progressStatusEvents chan VarEvent[string]
+}
+
+func newGeneratePostsWithProgressEventCollector() *GeneratePostsWithProgressEventCollector {
+ return &GeneratePostsWithProgressEventCollector{
+ blockEvents: make(chan BlockEvent, 100),
+ progressStatusEvents: make(chan VarEvent[string], 100),
+ }
+}
+
+func (c *GeneratePostsWithProgressEventCollector) BlockEvents() <-chan BlockEvent {
+ return c.blockEvents
+}
+
+func (c *GeneratePostsWithProgressEventCollector) ProgressStatusEvents() <-chan VarEvent[string] {
+ return c.progressStatusEvents
+}
+
+type MakePostsEventCollector struct {
+ blockEvents chan BlockEvent
+ progressPercentEvents chan VarEvent[int]
+ FunctionGeneratePostsWithProgress *GeneratePostsWithProgressEventCollector
+}
+
+func NewMakePostsEventCollector() *MakePostsEventCollector {
+ return &MakePostsEventCollector{
+ blockEvents: make(chan BlockEvent, 100),
+ progressPercentEvents: make(chan VarEvent[int], 100),
+ FunctionGeneratePostsWithProgress: newGeneratePostsWithProgressEventCollector(),
+ }
+}
+
+func (c *MakePostsEventCollector) BlockEvents() <-chan BlockEvent {
+ return c.blockEvents
+}
+
+func (c *MakePostsEventCollector) ProgressPercentEvents() <-chan VarEvent[int] {
+ return c.progressPercentEvents
+}
+```
+
+```go
+// main.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ b "example.com/myproject/baml_client"
+ "example.com/myproject/baml_client/events"
+)
+
+func main() {
+ ctx := context.Background()
+
+ // Create the main event collector
+ ev := events.NewMakePostsEventCollector()
+
+ // Consume subfunction streams as well as top-level updates.
+ go func() {
+ for block := range ev.FunctionGeneratePostsWithProgress.BlockEvents() {
+ fmt.Printf("Subfunction block: %s\n", block.BlockLabel)
+ }
+ }()
+
+ go func() {
+ for status := range ev.FunctionGeneratePostsWithProgress.ProgressStatusEvents() {
+ fmt.Printf("Subfunction progress: %s\n", status.Value)
+ }
+ }()
+
+ // Invoke the function
+ posts, err := b.MakePosts(ctx, "https://wikipedia.org/wiki/DNA", &b.MakePostsOptions{
+ Events: ev,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%+v\n", posts)
+}
+```
+
+
+
+## Track values across names
+
+In JavaScript and Python, values can be referenced by multiple names, and
+updating the value through one name will update it through the other names,
+too.
+
+```python Python
+x = { "name": "BAML" } # Make a python dict
+y = x # Alias x to a new name
+y["name"] = "Baml" # Modify the name
+assert(x["name"] == "Baml") # The original value is updated
+```
+
+The same rule applies to variables tagged with `@emit`. Anything causing a change to a value will
+cause the value to emit an event to listeners that subscribe to the original
+variable.
+
+```baml BAML
+let x = Foo { name: "BAML" } @emit; // Make a BAML value that auto-emits
+let y = x; // Alias x to a new name
+y.name = "Baml"; // Modify the new name => triggers event
+
+let a: int = 1 @emit; // Make a tracked BAML value
+let b = a; // Alias a to a new name
+b++; // Modify the new name => No new event
+ // (see Note below)
+```
+
+
+ Changes through a separate name for simple values like ints and strings,
+ on the other hand, wil not result in events being emitted, because when you
+ assign a new variable to an old variable holding plain data, the new variable
+ will receive a copy of the data, and modifying that copy will not affect
+ the original value.
+
+ As a rule of thumb, if a change to the new variable causes a change to the
+ old value, then the original variable will emit an event.
+
+
+## Track values that get packed into data structures
+
+If you put a value into a data structure, then modify it through that data structure,
+the value will continue to emit an event.
+
+```baml BAML
+let x = Foo { name: "BAML" } @emit; // Make a tracked BAML value
+let y = [x]; // Pack x into a list
+y[0].name = "Baml"; // Modify the list item => triggers event
+```
+
+Reminder: In Python and TypeScript, if you put a variable `x` into a list, then
+modify it through the list, printing `x` will show the modified value. So
+modifying `x` through `y[0]` above will also result in an event being emitted.
+
+## Track variables across function calls
+
+When you pass an `@emit` variable to a function, there are two possible outcomes
+if the called function modifies the variable:
+
+1. The modifications will be remembered by the system, but only the final
+ change to the variable will be emitted, and that will only happen when
+ the function returns. **OR:**
+1. The modification will immediately result in the firing of an event.
+
+You get to choose the behavior based on the needs of your workflow. If the function
+is doing some setup work that makes multiple changes to the emitted value to build
+it up to a valid result before the function returns, use Option 1 to hide the events
+from all those intermediate states. But if the sub-function is part of a workflow
+and you are using events to track all updates to your workflow's state, use Option 2
+to see all the intermediate updates in real time.
+
+
+ The event-emission behavior of Option 1 differs from the rule of thumb given
+ above about Python and TypeScript.
+ We offer two steparate options because there are legitimate cases where you would
+ not want the intermediate states to be emitted - for example if they violate
+ invariants of your type.
+
+
+To choose between modes, annotate the parameter with `@emit` in the function signature.
+
+```baml BAML
+function Main() -> int {
+ let state = Foo {
+ name: "BAML",
+ counter: 0,
+ } @emit; // Track state updates automatically
+ ReadState(state);
+ ChangeState(state);
+ 0
+}
+
+// This function uses Option 1, `state` in `Main()` will only fire one
+// event, when the function returns, even through `s` is modified twice.
+function ReadState(state: Foo) -> Foo {
+ state.counter++;
+ state.counter++;
+}
+
+// This function uses Option 2, the `s` parameter is
+// marked with `@emit`, so `state` in `Main()` will fire two events,
+// one for each update of `s`.
+function ChangeState(s: Foo @emit) -> null {
+ s.counter++;
+ s.name = "Baml";
+}
+```
+
+# Comparison with other event systems
+
+The `emit` system differs from many observability systems by focusing on automatic updates
+and typesafe event listeners. The ability to generate client code from your BAML
+programs is what allows us to create this tight integration.
+
+Let's compare BAML's observability to several other systems to get a better understanding
+of the trade-offs.
+
+## Logging and printf debugging
+
+The most common way of introspecting a running program is to add logging statements in
+your client's logging framework. Let's compare a simple example workflow in native
+Python to one instrumented in BAML.
+
+```python Python
+import logging
+from typing import List, Dict, Any
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+def LLMAnalizeSentiment(message: str) -> str: pass # Assumed function
+def LLMSummarizeSentiments(sentiments: List[str]) -> str: pass # Assumed function
+
+class Response(BaseModel):
+ sentiments: string[]
+ summary: string
+
+def analyze_sentiments(phrases: List[str]) -> Response:
+ logger.info(f"Starting analysis of {len(phrases)} phrases")
+
+ sentiments = []
+ for i, phrase in enumerate(phrases, 1):
+ logger.info(f"Analyzing phrase {i}/{len(phrases)}")
+ sentiment = LLMAnalizeSentiment(phrase)
+ sentiments.append({"phrase": phrase, "sentiment": sentiment})
+
+ logger.info("Generating summary")
+ summary = LLMSummarizeSentiments([s["sentiment"] for s in sentiments])
+
+ logger.info("Analysis complete")
+ return Response(sentiments=sentiments, summary=summary)
+```
+
+With BAML's block events, we don't need to mix explicit logging with the workflow
+logic. When a logged event needs extra context (such as the index of an item being
+processed from a list), we can use an `@emit` variable.
+
+```baml BAML
+function LLMAnalyzeSentiment(message: string) -> string { ... }
+function LLMSummarizeSentiments(message: string) -> string { ... }
+
+class Response {
+ sentiments string[]
+ summary string
+}
+
+function AnalyzeSentiments(messages: string[]) -> Response {
+ let status = "Starting analysis of " + messages.length().to_string() + " messages" @emit;
+
+ sentiments = []
+ for i, message in enumerate(messages, 1):
+ status = `Analyzing message ${i}/${messages.len()}`
+ sentiments.push(LLMAnalizeSentiment(message))
+
+ status = "Generating summary";
+ summary = LLMSummarizeSentiments([s["sentiment"] for s in sentiments])
+
+ status = "Analysis complete"
+ return Response(sentiments=sentiments, summary=summary)
+}
+```
+
+## Vercel AI SDK Generators
+
+In Vercel's AI SDK, you can use TypeScript generators to yield incremental updates during tool execution. The calling code can consume these yielded values to provide real-time feedback to users.
+
+```typescript TypeScript (Vercel AI SDK)
+import { UIToolInvocation, tool } from 'ai';
+import { z } from 'zod';
+
+export const weatherTool = tool({
+ description: 'Get the weather in a location',
+ inputSchema: z.object({ city: z.string() }),
+ async *execute({ city }: { city: string }) {
+ yield { state: 'loading' as const };
+
+ // Add artificial delay to simulate API call
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy'];
+ const weather =
+ weatherOptions[Math.floor(Math.random() * weatherOptions.length)];
+
+ yield {
+ state: 'ready' as const,
+ temperature: 72,
+ weather,
+ };
+ },
+});
+```
+
+The calling code streams these incremental updates by consuming the generator chunks:
+
+```typescript TypeScript (Consuming streaming yields)
+import { streamText } from 'ai';
+import { openai } from '@ai-sdk/openai';
+
+const stream = streamText({
+ model: openai('gpt-4'),
+ tools: { weather: weatherTool },
+ messages: [{ role: 'user', content: 'What is the weather in New York?' }],
+});
+
+// Stream individual yielded values as they arrive
+for await (const chunk of stream) {
+ if (chunk.type === 'tool-call-streaming-start') {
+ console.log('Weather tool started...');
+ } else if (chunk.type === 'tool-result') {
+ // Each yield from the generator appears here
+ if (chunk.result.state === 'loading') {
+ console.log('Weather lookup in progress...');
+ } else if (chunk.result.state === 'ready') {
+ console.log(`Weather: ${chunk.result.weather}, Temperature: ${chunk.result.temperature}°F`);
+ }
+ } else if (chunk.type === 'text-delta') {
+ // Stream the AI's text response
+ process.stdout.write(chunk.textDelta);
+ }
+}
+```
+
+This pattern provides a great mix of streaming and type safety. It differs architecturally
+from the pattern in BAML, where Workflow logic is separated from event handling logic.
+
+In BAML, functions and return values are meant for composing Workflow logic, while events
+are meant for communicating state back to your application. In the AI SDK, return values
+are used directly.
+
+**Key differences:**
+
+**Vercel AI SDK Generators:**
+- Manual yield statements at specific points in your tool execution
+- Generic streaming through the AI SDK's protocol
+- Tool-level progress updates handled by the framework
+- Updates tied to tool execution lifecycle
+
+**BAML's `emit`:**
+- Automatic event generation from variable assignments
+- Typesafe event listeners generated from your workflow code
+- Fine-grained control over exactly what business logic gets tracked
+- Updates tied to your specific domain logic and variable names
+
+
+## Mastra `.watch()`
+
+Mastra provides a `.watch()` method for monitoring workflow execution in real-time. Let's compare a workflow monitoring example using Mastra's approach to one using BAML's `emit` system.
+
+```typescript TypeScript (Mastra)
+// Mastra approach - watching workflow steps externally
+const workflow = mastra.createWorkflow(...)
+const run = await workflow.createRunAsync()
+
+run.watch((event) => {
+ console.log(`Step ${event?.payload?.currentStep?.id} completed`)
+ console.log(`Progress: ${event?.payload?.progress}`)
+})
+
+const result = await run.start({ inputData: { value: "initial data" } })
+```
+
+With BAML's `emit` system, you mark variables directly in your workflow logic and get typesafe event listeners generated for you.
+
+Both approaches enable real-time workflow monitoring. But Mastra's `watch()` function contains
+a more limited number of fields - telling you only about the Workflow stage you are in, not
+specific values being processed.
+
+## Comparison Table
+
+| Feature | Printf | Mastra | BAML |
+| --- | --- | --- | --- |
+| Real-time | 🟢 | 🟢 | 🟢 |
+| Streaming | ⚪ | 🟢 | 🟢 |
+| Debug levels | 🟢 | ⚪ | ⚪ |
+| Value subscription | ⚪ | ⚪ | 🟢 |
+| Typed listeners | ⚪ | ⚪ | 🟢 |
diff --git a/fern/03-reference/baml_client/runtime-events.mdx b/fern/03-reference/baml_client/runtime-events.mdx
new file mode 100644
index 0000000000..3072491523
--- /dev/null
+++ b/fern/03-reference/baml_client/runtime-events.mdx
@@ -0,0 +1,444 @@
+---
+title: Runtime Events
+---
+
+
+This feature was added in TODO
+
+
+The BAML runtime events system allows you to receive real-time callbacks about workflow execution, including block progress and variable updates. This enables you to build responsive UIs, track progress, and access intermediate results during complex BAML workflows.
+
+## Event Types
+
+### VarEvent
+
+Represents an update to an emitted variable in your BAML workflow.
+
+
+
+```python
+from typing import TypeVar, Generic
+from baml_client.types import VarEvent
+
+T = TypeVar('T')
+
+class VarEvent(Generic[T]):
+ """
+ Event fired when an emitted variable is updated
+
+ Attributes:
+ variable_name: Name of the variable that was updated
+ value: The new value of the variable
+ timestamp: ISO timestamp when the update occurred
+ function_name: Name of the BAML function containing the variable
+ """
+ variable_name: str
+ value: T
+ timestamp: str
+ function_name: str
+
+# Usage examples:
+# VarEvent[int] for integer variables
+# VarEvent[str] for string variables
+# VarEvent[List[Post]] for complex types
+```
+
+
+
+```typescript
+import type { VarEvent } from './baml-client/types'
+
+interface VarEvent {
+ /**
+ * Event fired when an emitted variable is updated
+ */
+
+ /** Name of the variable that was updated */
+ variableName: string
+
+ /** The new value of the variable */
+ value: T
+
+ /** ISO timestamp when the update occurred */
+ timestamp: string
+
+ /** Name of the BAML function containing the variable */
+ functionName: string
+}
+
+// Usage examples:
+// VarEvent for integer variables
+// VarEvent for string variables
+// VarEvent for complex types
+```
+
+
+
+```go
+package types
+
+import "time"
+
+// Since Go doesn't have user-defined generics, we generate specific types
+// for each emitted variable in your BAML functions
+
+// For a variable named "progress_percent" of type int
+type ProgressPercentVarEvent struct {
+ // Name of the variable that was updated
+ VariableName string `json:"variable_name"`
+
+ // The new value of the variable
+ Value int `json:"value"`
+
+ // Timestamp when the update occurred
+ Timestamp time.Time `json:"timestamp"`
+
+ // Name of the BAML function containing the variable
+ FunctionName string `json:"function_name"`
+}
+
+// For a variable named "current_task" of type string
+type CurrentTaskVarEvent struct {
+ VariableName string `json:"variable_name"`
+ Value string `json:"value"`
+ Timestamp time.Time `json:"timestamp"`
+ FunctionName string `json:"function_name"`
+}
+
+// For a variable named "completed_posts" of type []Post
+type CompletedPostsVarEvent struct {
+ VariableName string `json:"variable_name"`
+ Value []Post `json:"value"`
+ Timestamp time.Time `json:"timestamp"`
+ FunctionName string `json:"function_name"`
+}
+```
+
+
+
+### BlockEvent
+
+Represents progress through a markdown block in your BAML workflow.
+
+
+
+```python
+from baml_client.types import BlockEvent
+
+class BlockEvent:
+ """
+ Event fired when entering or exiting a markdown block
+
+ Attributes:
+ block_label: The markdown header text (e.g., "# Summarize Source")
+ block_level: The markdown header level (1-6)
+ event_type: Whether we're entering or exiting the block
+ timestamp: ISO timestamp when the event occurred
+ function_name: Name of the BAML function containing the block
+ """
+ block_label: str
+ block_level: int # 1-6 for # through ######
+ event_type: str # "enter" | "exit"
+ timestamp: str
+ function_name: str
+```
+
+
+
+```typescript
+import type { BlockEvent } from './baml-client/types'
+
+interface BlockEvent {
+ /**
+ * Event fired when entering or exiting a markdown block
+ */
+
+ /** The markdown header text (e.g., "# Summarize Source") */
+ blockLabel: string
+
+ /** The markdown header level (1-6) */
+ blockLevel: number
+
+ /** Whether we're entering or exiting the block */
+ eventType: "enter" | "exit"
+
+ /** ISO timestamp when the event occurred */
+ timestamp: string
+
+ /** Name of the BAML function containing the block */
+ functionName: string
+}
+```
+
+
+
+```go
+package types
+
+import "time"
+
+type BlockEventType string
+
+const (
+ BlockEventEnter BlockEventType = "enter"
+ BlockEventExit BlockEventType = "exit"
+)
+
+type BlockEvent struct {
+ // The markdown header text (e.g., "# Summarize Source")
+ BlockLabel string `json:"block_label"`
+
+ // The markdown header level (1-6)
+ BlockLevel int `json:"block_level"`
+
+ // Whether we're entering or exiting the block
+ EventType BlockEventType `json:"event_type"`
+
+ // Timestamp when the event occurred
+ Timestamp time.Time `json:"timestamp"`
+
+ // Name of the BAML function containing the block
+ FunctionName string `json:"function_name"`
+}
+```
+
+
+
+## Usage Examples
+
+### Tracking Variable Updates
+
+
+
+```python
+from baml_client import b, events
+from baml_client.types import VarEvent
+
+def track_progress(event: VarEvent[int]):
+ print(f"Progress updated: {event.value}% at {event.timestamp}")
+
+def track_current_task(event: VarEvent[str]):
+ print(f"Now working on: {event.value}")
+
+# Set up variable tracking
+ev = events.MakePosts()
+events.on_progress_percent(track_progress)
+events.on_current_task(track_current_task)
+
+# Run the function
+posts = await b.MakePosts("https://example.com", {"events": ev})
+```
+
+
+
+```typescript
+import { b, events } from './baml-client'
+import type { VarEvent } from './baml-client/types'
+
+const trackProgress = (event: VarEvent) => {
+ console.log(`Progress updated: ${event.value}% at ${event.timestamp}`)
+}
+
+const trackCurrentTask = (event: VarEvent) => {
+ console.log(`Now working on: ${event.value}`)
+}
+
+// Set up variable tracking
+const ev = events.MakePosts()
+events.on_progress_percent(trackProgress)
+events.on_current_task(trackCurrentTask)
+
+// Run the function
+const posts = await b.MakePosts("https://example.com", { events: ev })
+```
+
+
+
+```go
+package main
+
+import (
+ "fmt"
+ b "example.com/myproject/baml_client"
+ "example.com/myproject/baml_client/events"
+ "example.com/myproject/baml_client/types"
+)
+
+func trackProgress(event *types.ProgressPercentVarEvent) {
+ fmt.Printf("Progress updated: %d%% at %s\n",
+ event.Value, event.Timestamp.Format("15:04:05"))
+}
+
+func trackCurrentTask(event *types.CurrentTaskVarEvent) {
+ fmt.Printf("Now working on: %s\n", event.Value)
+}
+
+func main() {
+ ctx := context.Background()
+
+ // Set up variable tracking
+ ev := events.NewMakePosts()
+ events.OnProgressPercent(trackProgress)
+ events.OnCurrentTask(trackCurrentTask)
+
+ // Run the function
+ posts, err := b.MakePosts(ctx, "https://example.com", &b.MakePostsOptions{
+ Events: ev,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+```
+
+
+
+### Tracking Block Progress
+
+
+
+```python
+from baml_client import b, events
+from baml_client.types import BlockEvent
+
+def track_blocks(event: BlockEvent):
+ indent = " " * (event.block_level - 1)
+ action = "Starting" if event.event_type == "enter" else "Completed"
+ print(f"{indent}{action}: {event.block_label}")
+
+# Set up block tracking
+ev = events.MakePosts()
+events.on_block(track_blocks)
+
+# Run the function
+posts = await b.MakePosts("https://example.com", {"events": ev})
+```
+
+
+
+```typescript
+import { b, events } from './baml-client'
+import type { BlockEvent } from './baml-client/types'
+
+const trackBlocks = (event: BlockEvent) => {
+ const indent = " ".repeat(event.blockLevel - 1)
+ const action = event.eventType === "enter" ? "Starting" : "Completed"
+ console.log(`${indent}${action}: ${event.blockLabel}`)
+}
+
+// Set up block tracking
+const ev = events.MakePosts()
+events.on_block(trackBlocks)
+
+// Run the function
+const posts = await b.MakePosts("https://example.com", { events: ev })
+```
+
+
+
+```go
+func trackBlocks(event *types.BlockEvent) {
+ indent := strings.Repeat(" ", event.BlockLevel - 1)
+ action := "Starting"
+ if event.EventType == types.BlockEventExit {
+ action = "Completed"
+ }
+ fmt.Printf("%s%s: %s\n", indent, action, event.BlockLabel)
+}
+
+func main() {
+ ctx := context.Background()
+
+ // Set up block tracking
+ ev := events.NewMakePosts()
+ events.OnBlock(trackBlocks)
+
+ // Run the function
+ posts, err := b.MakePosts(ctx, "https://example.com", &b.MakePostsOptions{
+ Events: ev,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+```
+
+
+
+## Generated Event API
+
+
+
+When you run `baml generate`, BAML analyzes your functions and creates type-safe event handlers with generic types:
+
+```python
+# For a function with `emit let progress: int = 0`
+events.on_progress(callback: (event: VarEvent[int]) -> None)
+
+# For a function with `emit let status: string = "starting"`
+events.on_status(callback: (event: VarEvent[str]) -> None)
+
+# For all markdown blocks
+events.on_block(callback: (event: BlockEvent) -> None)
+```
+
+The generic `VarEvent[T]` type provides compile-time type safety, ensuring your event handlers receive the correct data types.
+
+
+
+When you run `baml generate`, BAML analyzes your functions and creates type-safe event handlers with generic types:
+
+```typescript
+// For a function with `emit let progress: int = 0`
+events.on_progress(callback: (event: VarEvent) => void)
+
+// For a function with `emit let status: string = "starting"`
+events.on_status(callback: (event: VarEvent) => void)
+
+// For all markdown blocks
+events.on_block(callback: (event: BlockEvent) => void)
+```
+
+The generic `VarEvent` interface provides compile-time type safety, ensuring your event handlers receive the correct data types.
+
+
+
+When you run `baml generate`, BAML analyzes your functions and creates specific types for each emitted variable (since Go doesn't have user-defined generics):
+
+```go
+// Separate types generated for each emitted variable
+type ProgressVarEvent struct {
+ VariableName string
+ Value int
+ Timestamp time.Time
+ FunctionName string
+}
+
+type StatusVarEvent struct {
+ VariableName string
+ Value string
+ Timestamp time.Time
+ FunctionName string
+}
+
+// Corresponding callback functions
+events.OnProgress(func(*types.ProgressVarEvent))
+events.OnStatus(func(*types.StatusVarEvent))
+events.OnBlock(func(*types.BlockEvent))
+```
+
+Each emitted variable gets its own dedicated event type, providing the same type safety as generics while working within Go's constraints.
+
+
+
+## Best Practices
+
+1. **Performance**: Keep event handlers lightweight. They run sequentially in
+ a separate thread from the rest of the BAML runtime
+1. **Error Handling**: Always include error handling in event callbacks
+1. **Naming**: Use descriptive names for emitted variables to generate clear event handler names
+
+## Related Topics
+
+- [Runtime Events Guide](/guide/baml-advanced/runtime-events) - Learn how to use events in workflows
+- [Collector](/ref/baml_client/collector) - Comprehensive logging system
\ No newline at end of file
diff --git a/fern/docs.yml b/fern/docs.yml
index 24a7296ead..15d98f65b7 100644
--- a/fern/docs.yml
+++ b/fern/docs.yml
@@ -412,6 +412,9 @@ navigation:
- page: Modular API
icon: fa-regular fa-cubes
path: 01-guide/05-baml-advanced/modular-api.mdx
+ - page: Runtime Events
+ icon: fa-regular fa-headset
+ path: 01-guide/05-baml-advanced/runtime-events.mdx
- section: Boundary Cloud
contents:
# - section: Functions
@@ -688,6 +691,9 @@ navigation:
path: 01-guide/05-baml-advanced/client-registry.mdx
- page: OnTick
path: 03-reference/baml_client/ontick.mdx
+ - page: Runtime Events
+ slug: events
+ path: 03-reference/baml_client/runtime-events.mdx
- page: Multimodal
slug: media
path: 03-reference/baml_client/media.mdx