> Version notice: This site documents BAML v0, the legacy DSL. BAML v1 is a separate, fully featured programming language in public beta. If the user intends to use BAML v1, ignore this site's content and use the BAML v1 documentation instead. If the intended version is unclear, clarify it before using this page. > For clean Markdown of any page, append .md to the page URL. For the complete documentation index, see https://docs.boundaryml.com/llms.txt. For MCP access, connect to https://docs.boundaryml.com/_mcp/server. # OnTick The `onTick` feature allows you to receive real-time callbacks during BAML function execution, providing access to internal state, streaming responses, and progress updates. This is particularly useful for monitoring function progress, debugging, and accessing intermediate data like "thinking" content from streaming LLM responses. ## Quick Start #### Python ```python from baml_client import b from baml_py import baml_py def on_tick(reason: str, log: baml_py.FunctionLog): print(f"Tick received: {reason}") print(f"Function calls: {len(log.calls) if log else 0}") # Use with async function result = await b.TestFunction("Hello world", baml_options={"on_tick": on_tick}) ``` #### TypeScript ```typescript import { b } from 'baml_client' import type { FunctionLog } from '@boundaryml/baml' type TickReason = "Unknown" const onTick = (reason: TickReason, log: FunctionLog | null) => { console.log(`Tick received: ${reason}`) console.log(`Function calls: ${log?.calls?.length || 0}`) } // Use with async function const result = await b.TestFunction("Hello world", { onTick }) ``` #### Go ```go import ( "fmt" b "my_project/baml_client" baml "github.com/boundaryml/baml/engine/language_client_go/pkg" ) func onTick(reason string, log *baml.FunctionLog) { fmt.Printf("Tick received: %s\n", reason) if log != nil { fmt.Printf("Function calls: %d\n", len(log.Calls)) } } // Use with function call result, err := b.TestFunction(ctx, "Hello world", b.WithOnTick(onTick)) ``` ## Common Use Cases ### Progress Monitoring Track the progress of long-running BAML function calls: #### Python ```python from baml_client import b from baml_py import baml_py def progress_monitor(reason: str, log: baml_py.FunctionLog): tick_count = getattr(progress_monitor, 'count', 0) progress_monitor.count = tick_count + 1 print(f"Progress tick #{progress_monitor.count}: {reason}") if log and log.calls: latest_call = log.calls[-1] print(f"Latest call to: {latest_call.client_name}") result = await b.ExtractResume( resume_text, baml_options={"on_tick": progress_monitor} ) ``` #### TypeScript ```typescript import { b } from 'baml_client' import type { FunctionLog } from '@boundaryml/baml' let tickCount = 0 const progressMonitor = (reason: string, log: FunctionLog | null) => { tickCount++ console.log(`Progress tick #${tickCount}: ${reason}`) if (log?.calls?.length) { const latestCall = log.calls[log.calls.length - 1] console.log(`Latest call to: ${latestCall.clientName}`) } } const result = await b.ExtractResume(resumeText, { onTick: progressMonitor }) ``` #### Go ```go var tickCount int func progressMonitor(reason string, log *baml.FunctionLog) { tickCount++ fmt.Printf("Progress tick #%d: %s\n", tickCount, reason) if log != nil && len(log.Calls) > 0 { latestCall := log.Calls[len(log.Calls)-1] fmt.Printf("Latest call to: %s\n", latestCall.ClientName) } } result, err := b.ExtractResume(ctx, resumeText, baml.WithOnTick(progressMonitor)) ``` ### Accessing Streaming "Thinking" Content Extract intermediate "thinking" content from streaming LLM responses: #### Python ```python import json from baml_client import b from baml_py import baml_py def extract_thinking(reason: str, log: baml_py.FunctionLog): thinking_content = "" if log and log.calls: last_call = log.calls[-1] # Check if it's a streaming call if hasattr(last_call, "sse_responses"): sse_responses = last_call.sse_responses() if sse_responses: for response in sse_responses: try: data = json.loads(response.text) if "delta" in data and "thinking" in data["delta"]: thinking_content += data["delta"]["thinking"] except (json.JSONDecodeError, AttributeError): pass if thinking_content: print(f"Thinking content: {thinking_content}") # Use with streaming function stream = b.stream.TestThinking( "Write a story about AI", baml_options={"on_tick": extract_thinking} ) async for msg in stream: pass result = await stream.get_final_response() ``` #### TypeScript ```typescript import { b } from 'baml_client' import type { FunctionLog, LlmStreamCall } from '@boundaryml/baml' const extractThinking = (reason: string, log: FunctionLog | null) => { let thinkingContent = "" if (log?.calls?.length) { const lastCall = log.calls[log.calls.length - 1] // Check if it's a stream call if ('sseResponses' in lastCall) { const streamCall = lastCall as LlmStreamCall const responses = streamCall.sseResponses() if (responses) { for (const response of responses) { try { const data = JSON.parse(response.text) if (data.delta?.thinking) { thinkingContent += data.delta.thinking } } catch { // Ignore parse errors } } } } } if (thinkingContent) { console.log(`Thinking content: ${thinkingContent}`) } } // Use with streaming function const stream = b.stream.TestThinking("Write a story about AI", { onTick: extractThinking }) for await (const msg of stream) { // Process streaming messages } const result = await stream.getFinalResponse() ``` #### Go ```go import ( "encoding/json" "fmt" "github.com/BoundaryML/baml/baml-go" ) func extractThinking(reason string, log *baml.FunctionLog) { thinkingContent := "" if log != nil && len(log.Calls) > 0 { lastCall := log.Calls[len(log.Calls)-1] // Check if it's a streaming call if streamCall, ok := lastCall.(*baml.LLMStreamCall); ok { responses := streamCall.SSEResponses() for _, response := range responses { var data map[string]interface{} if err := json.Unmarshal([]byte(response.Text), &data); err == nil { if delta, ok := data["delta"].(map[string]interface{}); ok { if thinking, ok := delta["thinking"].(string); ok { thinkingContent += thinking } } } } } } if thinkingContent != "" { fmt.Printf("Thinking content: %s\n", thinkingContent) } } // Use with streaming function stream, err := b.StreamTestThinking(ctx, "Write a story about AI", baml.WithOnTick(extractThinking)) if err != nil { return err } for msg := range stream.Channel() { // Process streaming messages } result := stream.FinalResponse() ``` ### Debugging and Logging Use onTick for comprehensive debugging and logging: #### Python ```python from baml_client import b from baml_py import baml_py def debug_logger(reason: str, log: baml_py.FunctionLog): print(f"=== DEBUG TICK: {reason} ===") if log: print(f"Function: {log.function_name}") print(f"Log type: {log.log_type}") print(f"Number of calls: {len(log.calls)}") if log.usage: print(f"Input tokens: {log.usage.input_tokens}") print(f"Output tokens: {log.usage.output_tokens}") if log.calls: latest_call = log.calls[-1] print(f"Latest provider: {latest_call.provider}") print(f"Latest client: {latest_call.client_name}") if latest_call.usage: print(f"Call usage - Input: {latest_call.usage.input_tokens}, Output: {latest_call.usage.output_tokens}") print("=== END DEBUG ===\n") result = await b.TestFunction("Debug this call", baml_options={"on_tick": debug_logger}) ``` #### TypeScript ```typescript import { b } from 'baml_client' import type { FunctionLog } from '@boundaryml/baml' const debugLogger = (reason: string, log: FunctionLog | null) => { console.log(`=== DEBUG TICK: ${reason} ===`) if (log) { console.log(`Function: ${log.functionName}`) console.log(`Log type: ${log.logType}`) console.log(`Number of calls: ${log.calls?.length || 0}`) if (log.usage) { console.log(`Input tokens: ${log.usage.inputTokens}`) console.log(`Output tokens: ${log.usage.outputTokens}`) } if (log.calls?.length) { const latestCall = log.calls[log.calls.length - 1] console.log(`Latest provider: ${latestCall.provider}`) console.log(`Latest client: ${latestCall.clientName}`) if (latestCall.usage) { console.log(`Call usage - Input: ${latestCall.usage.inputTokens}, Output: ${latestCall.usage.outputTokens}`) } } } console.log("=== END DEBUG ===\n") } const result = await b.TestFunction("Debug this call", { onTick: debugLogger }) ``` #### Go ```go func debugLogger(reason string, log *baml.FunctionLog) { fmt.Printf("=== DEBUG TICK: %s ===\n", reason) if log != nil { fmt.Printf("Function: %s\n", log.FunctionName) fmt.Printf("Log type: %s\n", log.LogType) fmt.Printf("Number of calls: %d\n", len(log.Calls)) if log.Usage != nil { fmt.Printf("Input tokens: %d\n", log.Usage.InputTokens) fmt.Printf("Output tokens: %d\n", log.Usage.OutputTokens) } if len(log.Calls) > 0 { latestCall := log.Calls[len(log.Calls)-1] fmt.Printf("Latest provider: %s\n", latestCall.Provider) fmt.Printf("Latest client: %s\n", latestCall.ClientName) if latestCall.Usage != nil { fmt.Printf("Call usage - Input: %d, Output: %d\n", latestCall.Usage.InputTokens, latestCall.Usage.OutputTokens) } } } fmt.Println("=== END DEBUG ===\n") } result, err := b.TestFunction(ctx, "Debug this call", baml.WithOnTick(debugLogger)) ``` ## Using with Collectors OnTick can be used alongside [Collectors](/ref/baml_client/collector) for comprehensive logging: #### Python ```python from baml_client import b from baml_py import baml_py, Collector def on_tick_with_collector(reason: str, log: baml_py.FunctionLog): print(f"OnTick fired: {reason}") # Create a collector alongside onTick collector = Collector("my-collector") result = await b.TestFunction( "Hello world", baml_options={ "on_tick": on_tick_with_collector, "collector": collector } ) # Access data through both mechanisms print(f"Collector usage: {collector.last.usage}") ``` #### TypeScript ```typescript import { b } from 'baml_client' import { Collector } from '@boundaryml/baml' import type { FunctionLog } from '@boundaryml/baml' const onTickWithCollector = (reason: string, log: FunctionLog | null) => { console.log(`OnTick fired: ${reason}`) } // Create a collector alongside onTick const collector = new Collector("my-collector") const result = await b.TestFunction("Hello world", { onTick: onTickWithCollector, collector }) // Access data through both mechanisms console.log(`Collector usage: ${collector.last?.usage}`) ``` #### Go ```go func onTickWithCollector(reason string, log *baml.FunctionLog) { fmt.Printf("OnTick fired: %s\n", reason) } // Create a collector alongside onTick collector, err := baml.NewCollector("my-collector") if err != nil { return err } result, err := b.TestFunction(ctx, "Hello world", baml.WithOnTick(onTickWithCollector), baml.WithCollector(collector), ) // Access data through both mechanisms fmt.Printf("Collector usage: %v\n", collector.Last().Usage) ``` ## Error Handling OnTick callbacks should handle errors gracefully. If an onTick callback throws an error, the function execution will continue: #### Python ```python from baml_client import b from baml_py import baml_py def error_prone_tick(reason: str, log: baml_py.FunctionLog): # Simulate an error condition if hasattr(error_prone_tick, 'count'): error_prone_tick.count += 1 else: error_prone_tick.count = 1 if error_prone_tick.count == 5: raise ValueError("Intentional error in onTick") print(f"Tick #{error_prone_tick.count}: {reason}") # Function will complete despite callback errors result = await b.TestFunction("Hello world", baml_options={"on_tick": error_prone_tick}) print("Function completed successfully despite onTick error") ``` #### TypeScript ```typescript import { b } from 'baml_client' import type { FunctionLog } from '@boundaryml/baml' let tickCount = 0 const errorProneTick = (reason: string, log: FunctionLog | null) => { tickCount++ if (tickCount === 5) { throw new Error("Intentional error in onTick") } console.log(`Tick #${tickCount}: ${reason}`) } // Function will complete despite callback errors const result = await b.TestFunction("Hello world", { onTick: errorProneTick }) console.log("Function completed successfully despite onTick error") ``` #### Go ```go var tickCount int func errorProneTick(reason string, log *baml.FunctionLog) { tickCount++ if tickCount == 5 { panic("Intentional error in onTick") // In Go, you might handle this differently } fmt.Printf("Tick #%d: %s\n", tickCount, reason) } // Function will complete despite callback errors result, err := b.TestFunction(ctx, "Hello world", baml.WithOnTick(errorProneTick)) if err == nil { fmt.Println("Function completed successfully despite onTick error") } ``` ## Limitations > **Warning** > > Keep these limitations in mind when using onTick: 1. **Synchronous Functions**: OnTick is **not supported** for synchronous function calls. Attempting to use onTick with sync functions will throw an error. 2. **Error Isolation**: Errors in onTick callbacks do not stop function execution, but they may not be explicitly surfaced. ## API Reference ### OnTick Callback Signature #### Python ```python def on_tick(reason: str, log: baml_py.FunctionLog | None) -> None: """ OnTick callback function Args: reason: The reason for the tick (currently always "Unknown") log: The current function log with call information """ pass ``` #### TypeScript ```typescript type TickCallback = (reason: TickReason, log: FunctionLog | null) => void type TickReason = "Unknown" // Currently only one reason type interface BamlCallOptions { onTick?: TickCallback // ... other options } ``` #### Go ```go type TickCallback func(reason string, log *FunctionLog) func WithOnTick(onTick TickCallback) CallOptionFunc func WithExperimentalOnTick(onTick TickCallback) CallOptionFunc // Deprecated ``` ### Integration with Function Calls OnTick is passed via the `baml_options` parameter (Python) or options object (TypeScript/Go): #### Python ```python # Async function call result = await b.FunctionName(input, baml_options={"on_tick": callback}) # Streaming function call stream = b.stream.FunctionName(input, baml_options={"on_tick": callback}) ``` #### TypeScript ```typescript // Async function call const result = await b.FunctionName(input, { onTick: callback }) // Streaming function call const stream = b.stream.FunctionName(input, { onTick: callback }) ``` #### Go ```go // Function call result, err := b.FunctionName(ctx, input, baml.WithOnTick(callback)) // Streaming function call stream, err := b.StreamFunctionName(ctx, input, baml.WithOnTick(callback)) ``` ## Related Topics * [Collector](/ref/baml_client/collector) - Learn about comprehensive logging with Collectors * [Using with\_options](/ref/baml_client/with-options) - Configure global options for BAML functions * [Streaming](/docs/calling-baml/streaming) - Understand streaming function calls ## Best Practices 1. **Keep Callbacks Light**: OnTick callbacks should be fast and non-blocking 2. **Handle Errors Gracefully**: Always include error handling in your callbacks 3. **Use with Collectors**: Combine onTick with Collectors for comprehensive logging 4. **Monitor Performance**: Test the performance impact for your specific use case 5. **Async Only**: Remember that onTick only works with async function calls, not sync calls