Chat Event Handlers
Hook into the chat lifecycle with additive callbacks for UI updates, logging, and analytics
After reading this guide, you will know:
- Which lifecycle events you can register handlers for.
- Why callbacks are additive and what replaced the 1.x
on_*handlers. - How chat callbacks differ from retry- and cancellation-safe usage instrumentation.
- How to observe tool calls, their progress, and tool results as they happen.
- How to observe messages as they are queued, join the conversation, or are withdrawn.
- How to observe model fallback attempts.
- When callbacks fire for streaming versus non-streaming requests.
Use callbacks to update a UI, log tool activity, or observe model fallbacks. They use the same before_ and after_ naming as Rails callbacks.
To replace files a protocol cannot send, use convert_unsupported_attachments. See Converting Unsupported Attachments for its return values and how it preserves the original conversation.
Message Events
chat = RubyLLM.chat
chat.before_message { print "Assistant > " }
chat.after_message { |message| puts message.content }
chat.ask "What is metaprogramming in Ruby?"
Message callbacks run for assistant responses and tool results, with or without streaming. They are additive: registering a second block keeps the first, along with RubyLLM’s own persistence callbacks.
Tool Events
Show which tools the model calls and what they return:
chat.before_tool_call do |call|
puts "Calling #{call.name} with #{call.arguments}"
end
chat.after_tool_result do |result|
puts "Tool returned: #{result}"
end
A slow tool can report what it is doing before its result arrives:
chat.after_tool_progress do |call, progress|
puts "#{call.name}: #{progress.message}"
end
See Reporting Progress.
Queue Events
Queued messages wait outside the conversation until the loop takes them. Three callbacks follow them:
chat.after_queue { |queued| puts "Queued: #{queued.content}" }
chat.after_dequeue { |message, queued| puts "Joined: #{message.content}" }
chat.after_unqueue { |queued| puts "Withdrawn: #{queued.content}" }
after_queue runs once queue has stored the message, and after_unqueue once unqueue has taken it back, each in the thread that called it. after_dequeue runs in the loop’s thread when a queued message joins the conversation. It receives the user message it became, then the queued message, before before_message runs for the reply that answers it. On a persisted chat, each callback receives the records: the RubyLLM::ActiveRecord::QueuedMessage, and for after_dequeue the message record first.
Fallback Events
chat.before_fallback do |fallback|
puts "Trying #{fallback.to.id} after #{fallback.from.id} failed"
end
chat.after_fallback do |fallback|
puts "#{fallback.to.id}: #{fallback.succeeded? ? 'succeeded' : 'failed'}"
end
before_fallback runs before each fallback attempt; after_fallback runs after it succeeds or fails. Both receive a RubyLLM::Fallback with from, to, error, attempt, response, fallback_error, streaming?, and chunks_yielded?.
Usage Events
after_message observes transcript changes. A cancelled request may produce no message, so use usage.ruby_llm for accounting across provider attempts, including retries and cancellations. See Cost and Usage Tracking and Instrumentation.
The 1.x on_* handlers were replaced in 2.0. See the 2.0 upgrade guide for the name changes.
Next Steps
- Chat - the core conversation interface these events fire on.
- Streaming - stream chunks as the assistant generates them.
- Cost and Usage Tracking - observe provider attempts independently from messages.
- Tools - define the tools whose calls and results these callbacks observe.
- Error Handling - configure model fallbacks and fallback error handling.
- Rails Integration - see how persistence callbacks run alongside your own.