diff --git a/.gitignore b/.gitignore
index 6cae97504c..c4e17613b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -171,3 +171,4 @@ jj-workflow.mdc
third_party
baml_repl_history.txt
+trace_events_debug.json
diff --git a/fern/01-guide/05-baml-advanced/runtime-observability.mdx b/fern/01-guide/05-baml-advanced/runtime-observability.mdx
new file mode 100644
index 0000000000..9a0b3ffec8
--- /dev/null
+++ b/fern/01-guide/05-baml-advanced/runtime-observability.mdx
@@ -0,0 +1,1447 @@
+---
+title: Runtime Observability
+---
+
+
+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 a notification 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 Block Notifications
+
+You can watch block notifications from your client code.
+
+When you generate client code from your BAML code, we produce watchers
+that allow you to hook in to its execution.
+
+
+
+
+In your client code, you can bind notifications to callbacks:
+
+```python Python
+ # baml_client/watchers.py
+from typing import TypeVar, Generic, Callable, Union
+
+class BlockNotification:
+ """
+ Notification fired when entering or exiting a markdown block
+ """
+ block_label: str
+ event_type: str # "enter" | "exit"
+
+class MakePosts:
+ """Watcher for MakePosts function"""
+
+ def on_block(self, handler: Callable[[BlockNotification], None]) -> None:
+ """Register a handler for block notification"""
+ pass
+```
+
+```python Python
+ # app.py
+ from baml_client.sync_client import { b }
+ from baml_client.types import Notification
+ import baml_client.watchers
+
+ def Example():
+ # Get a watcher with the right type for your MakePosts() function.
+ watcher = watchers.MakePosts()
+
+ # Associate the block notification with your own callback.
+ watcher.on_block(lambda notif: print(notif.block_label))
+
+ # Invoke the function.
+ posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"watchers": watcher})
+ print(posts)
+```
+
+
+
+In your client code, you can bind notifications to callbacks:
+
+```typescript
+// baml_client/watchers.ts
+export interface BlockNotification {
+ block_label: string;
+ event_type: "enter" | "exit";
+}
+
+export interface MakePosts {
+ // Register a handler for block notifications.
+ on_block(handler: (notif: BlockNotification) => void): void;
+}
+```
+
+```typescript
+ // index.ts
+ import { b, watchers } from "./baml-client"
+ import type { Notification } from "./baml-client/types"
+
+ async function Example() {
+ // Get a watcher with the right type
+ // for your MakePosts() function.
+ let watcher = watchers.MakePosts()
+
+ // Associate the block notification with your own callback.
+ watcher.on_block((notif) => {
+ console.log(notif.block_label)
+ });
+
+ // Invoke the function.
+ const posts = await b.MakePosts(
+ "https://wikipedia.org/wiki/DNA",
+ {"watchers": watcher}
+ )
+ console.log(posts)
+ }
+```
+
+
+
+In your client code, you can consume notifications via channels:
+
+```go
+// baml_client/watchers.go
+package watchers
+
+type BlockNotification struct {
+ BlockLabel string `json:"block_label"`
+ EventType string `json:"event_type"` // "enter" | "exit"
+}
+
+type MakePosts struct {
+ blockNotifications chan BlockNotification
+ // ... additional notification channels are initialized elsewhere.
+}
+
+func NewMakePosts() *MakePosts {
+ return &MakePosts{
+ blockNotifications: make(chan BlockNotification, 100),
+ }
+}
+
+// BlockNotifications provides block execution updates as a channel.
+func (c *MakePosts) BlockNotifications() <-chan BlockNotification {
+ return c.blockNotifications
+}
+```
+
+```go
+// main.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ b "example.com/myproject/baml_client"
+ "example.com/myproject/baml_client/watchers"
+)
+
+func main() {
+ ctx := context.Background()
+
+ // Get a watcher with the right channels
+ // for your MakePosts() function.
+ watcher := watchers.NewMakePosts()
+
+ // Consume block notifications asynchronously so updates are printed as they arrive.
+ go func() {
+ for blockNotif := range watcher.BlockNotifications() {
+ fmt.Println(blockNotif.BlockLabel)
+ }
+ }()
+
+ // Invoke the function.
+ posts, err := b.MakePosts(ctx, "https://wikipedia.org/wiki/DNA", &b.MakePostsOptions{
+ Watchers: watcher,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%+v\n", posts)
+}
+```
+
+
+
+## Track variables with `@watch`
+
+Variable updates can also be tracked with notifications. To mark an update for tracking,
+attach `@watch` to the variable declaration (or update its options) so the runtime knows to emit changes.
+
+```baml BAML
+let foo = State { counter: 0 } @watch;
+foo.counter += 1; // *** This triggers a notification
+```
+
+
+ ```python
+ watcher.vars.foo(lambda st: my_state_update_handler(st))
+ ```
+
+ ```typescript
+ watcher.on_var_foo((st) => my_state_update_handler(st))
+ ```
+
+ ```go
+ // Consume notifications from the watcher.FooNotifications channel
+ for st := range watcher.FooNotifications() {
+ handleState(st)
+ }
+ ```
+
+
+Updates can be tracked automatically or manually, depending on the `@watch` options you choose.
+Automatic tracking will emit notifications any time a variable is updated. Manual tracking
+only emits notifications 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 @watch; // *** Watch 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 notifications visible to the client.
+ progress_percent = i * 100 / posts_target_length
+ }
+}
+```
+
+### Watch parameters
+
+The variable tracking can be controled in several ways.
+
+ - `@watch(when=MyFilterFunc)` - Only emits when `MyFilterFunc` returns `true`
+ - `@watch(when=manual)` - Never auto emit (only emit when manually triggered)
+ - `@watch(skip_def=true)` - Emits every time the variable is updated, but not on initialization
+ - `@watch(name=my_channel)` - Emits notifications 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 notification 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 notifications for you.
+You could replicate the same behavior by using `@watch(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 `@watch(when=manual)` when initializing the variable, and
+call `.watchers.$notify()` on the variable when you want to manually trigger a notification.
+
+
+```baml BAML
+function ConstructValue(description: string) -> Character {
+ let character = Character { name: "", age: 0, skills: [] } @watch(when=manual);
+ character.name = LLMChooseName(description);
+ character.age = LLMChooseAge(description);
+ character.skills = LLMChooseSkills(description);
+ character.watchers.$notify() // *** Only notify when done building the character.
+}
+```
+
+### Sharing a Channel
+
+Sometimes you want multiple variables to send update notifications 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" @watch(name=updates);
+ let progress = 0 @watch(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 Notifications from Client Code
+
+
+
+
+
+When you generate a BAML client for our original function, your Python SDK will
+include a `MakePosts` watcher 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 `@watch`. The callbacks will receive an `int` data payload,
+because `progress_percent` is an `int`.
+
+```python
+# baml_client/watchers.py
+
+T = TypeVar('T')
+
+class VarNotification(Generic[T]):
+ """
+ Notification fired when a watched variable is updated
+ """
+ value: T
+ timestamp: str
+ function_name: str
+
+class MakePostsVarsCollector:
+ progress_percent: Callable[[VarNotification[int]], None]
+
+class MakePosts:
+ """Watcher for MakePosts function"""
+ vars: MakePostsVarsCollector
+```
+
+```python
+# app.py
+from baml_client.sync_client import { b }
+from baml_client.types import Notification
+import baml_client.watchers
+
+def Example():
+ # Get a watcher with the right type
+ # for your MakePosts() function.
+ watcher = watchers.MakePosts()
+
+ # Track the progress_percent variable updates
+ watcher.vars.progress_percent(lambda percent: print(f"Progress: {percent}%"))
+
+ # Invoke the function.
+ posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"watchers": watcher})
+ print(posts)
+```
+
+
+
+
+When you generate a BAML client for this function, its `MakePosts` watcher
+will accept callbacks for `progress_percent` because we marked that variable with
+`@watch`, and the callbacks will receive an `int` data payload, because
+`progress_percent` is an `int`.
+
+```typescript
+// baml_client/watchers.ts
+import { VarNotification } from "./types"
+
+export interface MakePosts {
+ on_var_progress_percent(callback: (percent: number) => void): void
+}
+
+export function MakePosts(): MakePosts {
+ return {
+ on_var_progress_percent(callback: (percent: number) => void): void {
+ // Implementation details
+ }
+ }
+}
+```
+
+```typescript
+// index.ts
+import { b, watchers } from "./baml-client"
+import type { VarNotification } from "./baml-client/types"
+
+async function Example() {
+ // Get a watcher with the right type
+ // for your MakePosts() function.
+ let watcher = watchers.MakePosts()
+
+ // Track the progress_percent variable updates
+ watcher.on_var_progress_percent((percent) => {
+ console.log(`Progress: ${percent}%`)
+ });
+
+ // Invoke the function.
+ const posts = await b.MakePosts(
+ "https://wikipedia.org/wiki/DNA",
+ {"watchers": watcher }
+ )
+ console.log(posts)
+}
+```
+
+
+
+
+In your client code, you can track these watched variables by constructing the
+generated watcher and reading from the channels it exposes.
+
+```go
+// baml_client/watchers.go
+package watchers
+
+import "time"
+
+type BlockNotification struct {
+ BlockLabel string `json:"block_label"`
+ EventType string `json:"event_type"` // "enter" | "exit"
+ Timestamp time.Time `json:"timestamp"`
+}
+
+type VarNotification[T any] struct {
+ VariableName string `json:"variable_name"`
+ Value T `json:"value"`
+ Timestamp time.Time `json:"timestamp"`
+ FunctionName string `json:"function_name"`
+}
+
+type MakePosts struct {
+ blockNotifications chan BlockNotification
+ progressPercentNotifications chan VarNotification[int]
+}
+
+func NewMakePosts() *MakePosts {
+ return &MakePosts{
+ blockNotifications: make(chan BlockNotification, 100),
+ progressPercentNotifications: make(chan VarNotification[int], 100),
+ }
+}
+
+// BlockNotifications returns block execution updates.
+func (c *MakePosts) BlockNotifications() <-chan BlockNotification {
+ return c.blockNotifications
+}
+
+// ProgressPercentNotifications streams progress_percent variable updates.
+func (c *MakePosts) ProgressPercentNotifications() <-chan VarNotification[int] {
+ return c.progressPercentNotifications
+}
+```
+
+```go
+// main.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ b "example.com/myproject/baml_client"
+ "example.com/myproject/baml_client/watchers"
+)
+
+func main() {
+ ctx := context.Background()
+
+ // Get a watcher with the right channels
+ // for your MakePosts() function.
+ watcher := watchers.NewMakePosts()
+
+ // Consume block notifications and progress updates concurrently.
+ go func() {
+ for block := range watcher.BlockNotifications() {
+ fmt.Printf("Block: %s\n", block.BlockLabel)
+ }
+ }()
+
+ go func() {
+ for percent := range watcher.ProgressPercentNotifications() {
+ fmt.Printf("Progress: %d%%\n", percent.Value)
+ }
+ }()
+
+ // Invoke the function.
+ posts, err := b.MakePosts(ctx, "https://wikipedia.org/wiki/DNA", &b.MakePostsOptions{
+ Watchers: watcher,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%+v\n", posts)
+}
+```
+
+
+
+For details about the types of notifications, see [BAML Language Reference](/ref/baml_client/watchers)
+
+# Streaming
+
+If updates to variables tagged with `@watch` 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 notification interface. Streaming notifications are available for all `@watch` variables,
+but they are generally only useful when assigning a variable from the result of
+an LLM function. All other streamed notifications 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)) @watch;
+ 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 watchers.
+
+
+
+
+```python
+# baml_client/watchers.py
+from typing import TypeVar, Generic, Callable
+from baml_client.types import BamlStream, VarNotification
+
+T = TypeVar('T')
+
+class VarNotification(Generic[T]):
+ """
+ Notification fired when a watched variable is updated
+ """
+ variable_name: str
+ value: T
+ timestamp: str
+ function_name: str
+
+class BlockNotification:
+ """
+ Notification fired when entering or exiting a markdown block
+ """
+ block_label: str
+ event_type: str # "enter" | "exit"
+
+class MakePostsVarsCollector:
+ progress_percent: Callable[[VarNotification[int]], None]
+
+class DescribeTerminatorMovies:
+ """Watcher for DescribeTerminatorMovies function with both regular and streaming notifications"""
+
+ def on_block(self, handler: Callable[[BlockNotification], None]) -> None:
+ """Register a handler for block notifications"""
+ pass
+
+ def on_var_movie_text(self, handler: Callable[[VarNotification[str]], None]) -> None:
+ """Register a handler for movie_text variable updates"""
+ pass
+
+ def on_stream_movie_text(self, handler: Callable[[BamlStream[VarNotification[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.watchers as watchers
+
+def example():
+ # Create the unified watcher
+ watcher = watchers.DescribeTerminatorMovies()
+
+ # Track streaming updates for the main watched variable
+ def handle_movie_text_stream(stream):
+ for notif in stream:
+ print(f"Streaming movie text: {notif.value}")
+
+ watcher.on_stream_movie_text(handle_movie_text_stream)
+
+ # Invoke the function with watchers
+ results = b.DescribeTerminatorMovies({"watchers": watcher})
+ print("Final results:", results)
+```
+
+
+
+
+```typescript
+// baml_client/watchers.ts
+import { BamlStream, VarNotification } from "./types";
+
+export interface BlockNotification {
+ block_label: string;
+ event_type: "enter" | "exit";
+}
+
+export interface VarNotification {
+ variable_name: string;
+ value: T;
+ timestamp: string;
+ function_name: string;
+}
+
+export interface DescribeTerminatorMovies {
+ // Regular notification handlers
+ on_block(handler: (notif: BlockNotification) => void): void;
+ on_var_movie_text(handler: (notif: VarNotification) => void): void;
+
+ // Streaming notification handlers
+ on_stream_movie_text(handler: (stream: BamlStream>) => void): void;
+}
+
+export function DescribeTerminatorMovies(): DescribeTerminatorMovies {
+ return {
+ on_block(handler: (notif: BlockNotification) => void): void {
+ // Implementation details
+ },
+ on_var_movie_text(handler: (notif: VarNotification) => void): void {
+ // Implementation details
+ },
+ on_stream_movie_text(handler: (stream: BamlStream>) => void): void {
+ // Implementation details
+ }
+ }
+}
+```
+
+```typescript
+// index.ts
+import { b, watchers } from "./baml-client"
+
+async function example() {
+ // Create the unified watcher
+ let watcher = watchers.DescribeTerminatorMovies()
+
+ // Track streaming updates for the main watched variable
+ watcher.on_stream_movie_text(async (stream) => {
+ for await (const notif of stream) {
+ console.log(`Streaming movie text: ${notif.value}`)
+ }
+ })
+
+ // Invoke the function with watchers
+ const results = await b.DescribeTerminatorMovies({"watchers": watcher})
+ console.log("Final results:", results)
+}
+```
+
+
+
+
+```go
+// baml_client/watchers.go
+package watchers
+
+import "time"
+
+type BlockNotification struct {
+ BlockLabel string `json:"block_label"`
+ EventType string `json:"event_type"` // "enter" | "exit"
+ Timestamp time.Time `json:"timestamp"`
+}
+
+type VarNotification[T any] struct {
+ VariableName string `json:"variable_name"`
+ Value T `json:"value"`
+ Timestamp time.Time `json:"timestamp"`
+ FunctionName string `json:"function_name"`
+}
+
+type DescribeTerminatorMovies struct {
+ blockNotifications chan BlockNotification
+ movieTextNotifications chan VarNotification[string]
+ movieTextStreams chan (<-chan VarNotification[string])
+}
+
+func NewDescribeTerminatorMovies() *DescribeTerminatorMovies {
+ return &DescribeTerminatorMovies{
+ blockNotifications: make(chan BlockNotification, 100),
+ movieTextNotifications: make(chan VarNotification[string], 100),
+ movieTextStreams: make(chan (<-chan VarNotification[string]), 10),
+ }
+}
+
+func (c *DescribeTerminatorMovies) BlockNotifications() <-chan BlockNotification {
+ return c.blockNotifications
+}
+
+func (c *DescribeTerminatorMovies) MovieTextNotifications() <-chan VarNotification[string] {
+ return c.movieTextNotifications
+}
+
+// MovieTextStreams produces a stream-of-streams for watched movie_text updates.
+func (c *DescribeTerminatorMovies) MovieTextStreams() <-chan (<-chan VarNotification[string]) {
+ return c.movieTextStreams
+}
+```
+
+```go
+// main.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ b "example.com/myproject/baml_client"
+ "example.com/myproject/baml_client/watchers"
+)
+
+func main() {
+ ctx := context.Background()
+
+ // Create the unified watcher
+ watcher := watchers.NewDescribeTerminatorMovies()
+
+ // Track block notifications and single-value updates concurrently.
+ go func() {
+ for block := range watcher.BlockNotifications() {
+ fmt.Printf("Block: %s\n", block.BlockLabel)
+ }
+ }()
+
+ go func() {
+ for movieText := range watcher.MovieTextNotifications() {
+ fmt.Printf("Variable movie text: %s\n", movieText.Value)
+ }
+ }()
+
+ // Track streaming updates using the channel-of-channels pattern.
+ go func() {
+ for stream := range watcher.MovieTextStreams() {
+ go func(inner <-chan watchers.VarNotification[string]) {
+ for notif := range inner {
+ fmt.Printf("Streaming movie text: %s\n", notif.Value)
+ }
+ }(stream)
+ }
+ }()
+
+ // Invoke the function with watchers
+ results, err := b.DescribeTerminatorMovies(ctx, &b.DescribeTerminatorMoviesOptions{
+ Watchers: watcher,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("Final results: %+v\n", results)
+}
+```
+
+
+
+## Combining Regular Notifications and Streaming
+
+You can use both regular notifications and streaming notifications together in a single unified watcher to get comprehensive observability:
+
+
+
+
+```python
+from baml_client.sync_client import b
+import baml_client.watchers as watchers
+
+def comprehensive_example():
+ # Create unified watcher
+ watcher = watchers.DescribeTerminatorMovies()
+
+ # Regular notifications for workflow progress
+ watcher.on_block(lambda block: print(f"Block: {block.block_label}"))
+ watcher.vars.movie_text(lambda movie_text: print(f"Variable movie text: {movie_text.value}"))
+
+ # Streaming notifications for real-time content
+ def handle_stream(stream):
+ for notif in stream:
+ print(f"Streaming content: {notif.value}")
+
+ watcher.on_stream_movie_text(handle_stream)
+
+ # Use single watchers parameter
+ results = b.DescribeTerminatorMovies({"watchers": watcher})
+```
+
+
+
+
+```typescript
+import { b, watchers } from "./baml-client"
+
+async function comprehensiveExample() {
+ // Create unified watcher
+ let watcher = watchers.DescribeTerminatorMovies()
+
+ // Regular notifications for workflow progress
+ watcher.on_block((block) => console.log(`Block: ${block.block_label}`))
+ watcher.on_var_movie_text((movieText) => console.log(`Variable movie text: ${movieText.value}`))
+
+ // Streaming notifications for real-time content
+ watcher.on_stream_movie_text(async (stream) => {
+ for await (const notif of stream) {
+ console.log(`Streaming content: ${notif.value}`)
+ }
+ })
+
+ // Use single watchers parameter
+ const results = await b.DescribeTerminatorMovies({ watchers: watcher })
+}
+```
+
+
+
+
+```go
+func comprehensiveExample() {
+ ctx := context.Background()
+
+ // Create unified watcher
+ watcher := watchers.NewDescribeTerminatorMovies()
+
+ // Regular notifications for workflow progress
+ go func() {
+ for block := range watcher.BlockNotifications() {
+ fmt.Printf("Block: %s\n", block.BlockLabel)
+ }
+ }()
+ go func() {
+ for movieText := range watcher.MovieTextNotifications() {
+ fmt.Printf("Variable movie text: %s\n", movieText.Value)
+ }
+ }()
+
+ // Streaming notifications for real-time content
+ go func() {
+ for stream := range watcher.MovieTextStreams() {
+ go func(inner <-chan watchers.VarNotification[string]) {
+ for notif := range inner {
+ fmt.Printf("Streaming content: %s\n", notif.Value)
+ }
+ }(stream)
+ }
+ }()
+
+ // Use single Watchers parameter
+ results, err := b.DescribeTerminatorMovies(ctx, &b.DescribeTerminatorMoviesOptions{
+ Watchers: watcher,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+```
+
+
+
+# Usage Scenarios
+
+## Track notifications from subfunctions
+
+When your main workflow calls other BAML functions, you can track notifications from those subfunctions as well. If `MakePosts()` calls `Foo()`, and `Foo()` contains variables tagged with `@watch` or markdown blocks, the client invoking `MakePosts()` can subscribe to those subfunction notifications through dedicated records in the watcher.
+
+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" @watch;
+
+ # 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/watchers.py
+
+class GeneratePostsWithProgress:
+ """Watcher for GeneratePostsWithProgress function"""
+
+ def on_block(self, handler: Callable[[BlockNotification], None]) -> None:
+ """Register a handler for block notifications from this function"""
+ pass
+
+ def on_var_progress_status(self, handler: Callable[[VarNotification[str]], None]) -> None:
+ """Register a handler for progress_status variable updates"""
+ pass
+
+class MakePosts:
+ """Watcher for MakePosts function"""
+
+ def __init__(self):
+ self.function_GeneratePostsWithProgress = GeneratePostsWithProgress()
+```
+
+```python
+# app.py
+from baml_client.sync_client import b
+import baml_client.watchers as watchers
+
+def example():
+ # Create the main watcher
+ watcher = watchers.MakePosts()
+
+ # Subscribe to subfunction notifications
+ watcher.function_GeneratePostsWithProgress.vars.progress_status(
+ lambda e: print(f"Subfunction progress: {e.value}")
+ )
+
+ watcher.function_GeneratePostsWithProgress.on_block(
+ lambda e: print(f"Subfunction block: {e.block_label}")
+ )
+
+ # Invoke the function
+ posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"watchers": watcher})
+ print(posts)
+```
+
+
+
+
+```typescript
+// baml_client/watchers.ts
+
+export interface GeneratePostsWithProgress {
+ on_block(handler: (notif: BlockNotification) => void): void;
+ on_var_progress_status(handler: (notif: VarNotification) => void): void;
+}
+
+export interface MakePosts {
+ function_GeneratePostsWithProgress: GeneratePostsWithProgress;
+}
+
+export function MakePosts(): MakePosts {
+ return {
+ function_GeneratePostsWithProgress: {
+ on_block(handler: (notif: BlockNotification) => void): void {
+ // Implementation details
+ },
+ on_var_progress_status(handler: (notif: VarNotification) => void): void {
+ // Implementation details
+ }
+ }
+ }
+}
+```
+
+```typescript
+// index.ts
+import { b, watchers } from "./baml-client"
+
+async function example() {
+ // Create the main watcher
+ let watcher = watchers.MakePosts()
+
+ // Subscribe to subfunction notifications
+ watcher.function_GeneratePostsWithProgress.on_var_progress_status((e) => {
+ console.log(`Subfunction progress: ${e.value}`)
+ })
+
+ watcher.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", {"watchers": watcher})
+ console.log(posts)
+}
+```
+
+
+
+
+```go
+// baml_client/watchers.go
+package watchers
+
+import "time"
+
+type BlockNotification struct {
+ BlockLabel string `json:"block_label"`
+ EventType string `json:"event_type"`
+ Timestamp time.Time `json:"timestamp"`
+}
+
+type VarNotification[T any] struct {
+ VariableName string `json:"variable_name"`
+ Value T `json:"value"`
+ Timestamp time.Time `json:"timestamp"`
+ FunctionName string `json:"function_name"`
+}
+
+type GeneratePostsWithProgress struct {
+ blockNotifications chan BlockNotification
+ progressStatusNotifications chan VarNotification[string]
+}
+
+func newGeneratePostsWithProgress() *GeneratePostsWithProgress {
+ return &GeneratePostsWithProgress{
+ blockNotifications: make(chan BlockNotification, 100),
+ progressStatusNotifications: make(chan VarNotification[string], 100),
+ }
+}
+
+func (c *GeneratePostsWithProgress) BlockNotifications() <-chan BlockNotification {
+ return c.blockNotifications
+}
+
+func (c *GeneratePostsWithProgress) ProgressStatusNotifications() <-chan VarNotification[string] {
+ return c.progressStatusNotifications
+}
+
+type MakePosts struct {
+ blockNotifications chan BlockNotification
+ progressPercentNotifications chan VarNotification[int]
+ FunctionGeneratePostsWithProgress *GeneratePostsWithProgress
+}
+
+func NewMakePosts() *MakePosts {
+ return &MakePosts{
+ blockNotifications: make(chan BlockNotification, 100),
+ progressPercentNotifications: make(chan VarNotification[int], 100),
+ FunctionGeneratePostsWithProgress: newGeneratePostsWithProgress(),
+ }
+}
+
+func (c *MakePosts) BlockNotifications() <-chan BlockNotification {
+ return c.blockNotifications
+}
+
+func (c *MakePosts) ProgressPercentNotifications() <-chan VarNotification[int] {
+ return c.progressPercentNotifications
+}
+```
+
+```go
+// main.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ b "example.com/myproject/baml_client"
+ "example.com/myproject/baml_client/watchers"
+)
+
+func main() {
+ ctx := context.Background()
+
+ // Create the main watcher
+ watcher := watchers.NewMakePosts()
+
+ // Consume subfunction streams as well as top-level updates.
+ go func() {
+ for block := range watcher.FunctionGeneratePostsWithProgress.BlockNotifications() {
+ fmt.Printf("Subfunction block: %s\n", block.BlockLabel)
+ }
+ }()
+
+ go func() {
+ for status := range watcher.FunctionGeneratePostsWithProgress.ProgressStatusNotifications() {
+ fmt.Printf("Subfunction progress: %s\n", status.Value)
+ }
+ }()
+
+ // Invoke the function
+ posts, err := b.MakePosts(ctx, "https://wikipedia.org/wiki/DNA", &b.MakePostsOptions{
+ Watchers: watcher,
+ })
+ 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 `@watch`. Anything causing a change to a value will
+cause the value to emit a notification to listeners that subscribe to the original
+variable.
+
+```baml BAML
+let x = Foo { name: "BAML" } @watch; // Make a BAML value that auto-emits
+let y = x; // Alias x to a new name
+y.name = "Baml"; // Modify the new name => triggers notification
+
+let a: int = 1 @watch; // Make a tracked BAML value
+let b = a; // Alias a to a new name
+b++; // Modify the new name => No new notification
+ // (see Note below)
+```
+
+
+ Changes through a separate name for simple values like ints and strings,
+ on the other hand, wil not result in notifications 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 a notification.
+
+
+## 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 a notification.
+
+```baml BAML
+let x = Foo { name: "BAML" } @watch; // Make a tracked BAML value
+let y = [x]; // Pack x into a list
+y[0].name = "Baml"; // Modify the list item => triggers notification
+```
+
+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 a notification being emitted.
+
+## Track variables across function calls
+
+When you pass a `@watch` 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 a notification.
+
+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 watched value to build
+it up to a valid result before the function returns, use Option 1 to hide the notifications
+from all those intermediate states. But if the sub-function is part of a workflow
+and you are using notifications to track all updates to your workflow's state, use Option 2
+to see all the intermediate updates in real time.
+
+
+ The notification-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 `@watch` in the function signature.
+
+```baml BAML
+function Main() -> int {
+ let state = Foo {
+ name: "BAML",
+ counter: 0,
+ } @watch; // Track state updates automatically
+ ReadState(state);
+ ChangeState(state);
+ 0
+}
+
+// This function uses Option 1, `state` in `Main()` will only fire one
+// notification, 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 `@watch`, so `state` in `Main()` will fire two notifications,
+// one for each update of `s`.
+function ChangeState(s: Foo @watch) -> null {
+ s.counter++;
+ s.name = "Baml";
+}
+```
+
+# Comparison with other observability systems
+
+The `@watch` system differs from many observability systems by focusing on automatic updates
+and typesafe notification 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 notifications, we don't need to mix explicit logging with the workflow
+logic. When a logged notification needs extra context (such as the index of an item being
+processed from a list), we can use a `@watch` 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" @watch;
+
+ 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 notification handling logic.
+
+In BAML, functions and return values are meant for composing Workflow logic, while notifications
+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 `@watch`:**
+- Automatic notification generation from variable assignments
+- Typesafe notification 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 `@watch` 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 `@watch` system, you mark variables directly in your workflow logic and get typesafe notification 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-observability.mdx b/fern/03-reference/baml_client/runtime-observability.mdx
new file mode 100644
index 0000000000..3043942dd7
--- /dev/null
+++ b/fern/03-reference/baml_client/runtime-observability.mdx
@@ -0,0 +1,459 @@
+---
+title: Runtime Observability
+---
+
+
+This feature was added in 0.210.0
+
+
+The BAML runtime observability 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.
+
+## Notification Types
+
+### VarNotification
+
+Represents an update to a watched variable in your BAML workflow.
+
+
+
+```python
+from typing import TypeVar, Generic
+from baml_client.types import VarNotification
+
+T = TypeVar('T')
+
+class VarNotification(Generic[T]):
+ """
+ Notification fired when a watched 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:
+# VarNotification[int] for integer variables
+# VarNotification[str] for string variables
+# VarNotification[List[Post]] for complex types
+```
+
+
+
+```typescript
+import type { VarNotification } from './baml-client/types'
+
+interface VarNotification {
+ /**
+ * Notification fired when a watched 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:
+// VarNotification for integer variables
+// VarNotification for string variables
+// VarNotification for complex types
+```
+
+
+
+```go
+package types
+
+import "time"
+
+// Since Go doesn't have user-defined generics, we generate specific types
+// for each watched variable in your BAML functions
+
+// For a variable named "progress_percent" of type int
+type ProgressPercentVarNotification 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 CurrentTaskVarNotification 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 CompletedPostsVarNotification struct {
+ VariableName string `json:"variable_name"`
+ Value []Post `json:"value"`
+ Timestamp time.Time `json:"timestamp"`
+ FunctionName string `json:"function_name"`
+}
+```
+
+
+
+### BlockNotification
+
+Represents progress through a markdown block in your BAML workflow.
+
+
+
+```python
+from baml_client.types import BlockNotification
+
+class BlockNotification:
+ """
+ Notification 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 { BlockNotification } from './baml-client/types'
+
+interface BlockNotification {
+ /**
+ * Notification 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 BlockNotificationType string
+
+const (
+ BlockNotificationEnter BlockNotificationType = "enter"
+ BlockNotificationExit BlockNotificationType = "exit"
+)
+
+type BlockNotification 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 BlockNotificationType `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, watchers
+from baml_client.types import VarNotification
+
+def track_progress(notif: VarNotification[int]):
+ print(f"Progress updated: {notif.value}% at {notif.timestamp}")
+
+def track_current_task(notif: VarNotification[str]):
+ print(f"Now working on: {notif.value}")
+
+# Set up variable tracking
+watcher = watchers.MakePosts()
+watcher.vars.progress_percent(track_progress)
+watcher.vars.current_task(track_current_task)
+
+# Run the function
+posts = await b.MakePosts("https://example.com", {"watchers": watcher})
+```
+
+
+
+```typescript
+import { b, watchers } from './baml-client'
+import type { VarNotification } from './baml-client/types'
+
+const trackProgress = (notif: VarNotification) => {
+ console.log(`Progress updated: ${notif.value}% at ${notif.timestamp}`)
+}
+
+const trackCurrentTask = (notif: VarNotification) => {
+ console.log(`Now working on: ${notif.value}`)
+}
+
+// Set up variable tracking
+const watcher = watchers.MakePosts()
+watcher.on_var_progress_percent(trackProgress)
+watcher.on_var_current_task(trackCurrentTask)
+
+// Run the function
+const posts = await b.MakePosts("https://example.com", { watchers: watcher })
+```
+
+
+
+```go
+package main
+
+import (
+ "fmt"
+ b "example.com/myproject/baml_client"
+ "example.com/myproject/baml_client/watchers"
+ "example.com/myproject/baml_client/types"
+)
+
+func trackProgress(notif *types.ProgressPercentVarNotification) {
+ fmt.Printf("Progress updated: %d%% at %s\n",
+ notif.Value, notif.Timestamp.Format("15:04:05"))
+}
+
+func trackCurrentTask(notif *types.CurrentTaskVarNotification) {
+ fmt.Printf("Now working on: %s\n", notif.Value)
+}
+
+func main() {
+ ctx := context.Background()
+
+ // Set up variable tracking
+ watcher := watchers.NewMakePosts()
+
+ go func() {
+ for notif := range watcher.ProgressPercentNotifications() {
+ trackProgress(notif)
+ }
+ }()
+
+ go func() {
+ for notif := range watcher.CurrentTaskNotifications() {
+ trackCurrentTask(notif)
+ }
+ }()
+
+ // Run the function
+ posts, err := b.MakePosts(ctx, "https://example.com", &b.MakePostsOptions{
+ Watchers: watcher,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+```
+
+
+
+### Tracking Block Progress
+
+
+
+```python
+from baml_client import b, watchers
+from baml_client.types import BlockNotification
+
+def track_blocks(notif: BlockNotification):
+ indent = " " * (notif.block_level - 1)
+ action = "Starting" if notif.event_type == "enter" else "Completed"
+ print(f"{indent}{action}: {notif.block_label}")
+
+# Set up block tracking
+watcher = watchers.MakePosts()
+watcher.on_block(track_blocks)
+
+# Run the function
+posts = await b.MakePosts("https://example.com", {"watchers": watcher})
+```
+
+
+
+```typescript
+import { b, watchers } from './baml-client'
+import type { BlockNotification } from './baml-client/types'
+
+const trackBlocks = (notif: BlockNotification) => {
+ const indent = " ".repeat(notif.blockLevel - 1)
+ const action = notif.eventType === "enter" ? "Starting" : "Completed"
+ console.log(`${indent}${action}: ${notif.blockLabel}`)
+}
+
+// Set up block tracking
+const watcher = watchers.MakePosts()
+watcher.on_block(trackBlocks)
+
+// Run the function
+const posts = await b.MakePosts("https://example.com", { watchers: watcher })
+```
+
+
+
+```go
+func trackBlocks(notif *types.BlockNotification) {
+ indent := strings.Repeat(" ", notif.BlockLevel - 1)
+ action := "Starting"
+ if notif.EventType == types.BlockNotificationExit {
+ action = "Completed"
+ }
+ fmt.Printf("%s%s: %s\n", indent, action, notif.BlockLabel)
+}
+
+func main() {
+ ctx := context.Background()
+
+ // Set up block tracking
+ watcher := watchers.NewMakePosts()
+
+ go func() {
+ for notif := range watcher.BlockNotifications() {
+ trackBlocks(notif)
+ }
+ }()
+
+ // Run the function
+ posts, err := b.MakePosts(ctx, "https://example.com", &b.MakePostsOptions{
+ Watchers: watcher,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+```
+
+
+
+## Generated Watcher API
+
+
+
+When you run `baml generate`, BAML analyzes your functions and creates type-safe notification handlers with generic types:
+
+```python
+# For a function with `@watch let progress: int = 0`
+watcher.vars.progress(callback: (notif: VarNotification[int]) -> None)
+
+# For a function with `@watch let status: string = "starting"`
+watcher.vars.status(callback: (notif: VarNotification[str]) -> None)
+
+# For all markdown blocks
+watcher.on_block(callback: (notif: BlockNotification) -> None)
+```
+
+The generic `VarNotification[T]` type provides compile-time type safety, ensuring your notification handlers receive the correct data types.
+
+
+
+When you run `baml generate`, BAML analyzes your functions and creates type-safe notification handlers with generic types:
+
+```typescript
+// For a function with `@watch let progress: int = 0`
+watcher.on_var_progress(callback: (notif: VarNotification) => void)
+
+// For a function with `@watch let status: string = "starting"`
+watcher.on_var_status(callback: (notif: VarNotification) => void)
+
+// For all markdown blocks
+watcher.on_block(callback: (notif: BlockNotification) => void)
+```
+
+The generic `VarNotification` interface provides compile-time type safety, ensuring your notification handlers receive the correct data types.
+
+
+
+When you run `baml generate`, BAML analyzes your functions and creates specific types for each watched variable (since Go doesn't have user-defined generics):
+
+```go
+// Separate types generated for each watched variable
+type ProgressVarNotification struct {
+ VariableName string
+ Value int
+ Timestamp time.Time
+ FunctionName string
+}
+
+type StatusVarNotification struct {
+ VariableName string
+ Value string
+ Timestamp time.Time
+ FunctionName string
+}
+
+// Corresponding notification channels
+watcher.ProgressNotifications() <-chan *types.ProgressVarNotification
+watcher.StatusNotifications() <-chan *types.StatusVarNotification
+watcher.BlockNotifications() <-chan *types.BlockNotification
+```
+
+Each watched variable gets its own dedicated notification type, providing the same type safety as generics while working within Go's constraints.
+
+
+
+## Best Practices
+
+1. **Performance**: Keep notification handlers lightweight. They run sequentially in
+ a separate thread from the rest of the BAML runtime
+1. **Error Handling**: Always include error handling in notification callbacks
+1. **Naming**: Use descriptive names for watched variables to generate clear notification handler names
+
+## Related Topics
+
+- [Runtime Observability Guide](/guide/baml-advanced/runtime-observability) - Learn how to use notifications 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..e74c33024c 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 Observability
+ icon: fa-regular fa-headset
+ path: 01-guide/05-baml-advanced/runtime-observability.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 Observability
+ slug: watchers
+ path: 03-reference/baml_client/runtime-observability.mdx
- page: Multimodal
slug: media
path: 03-reference/baml_client/media.mdx