Transcript hooks
A transcript hook runs inside Zimmer whenever new transcript messages are broadcast. It reads
the agent’s output and writes conclusions into session.custom_metadata.
The contract
Section titled “The contract”TranscriptHooks::BaseHook (app/services/transcript_hooks/base_hook.rb):
class MyHook < TranscriptHooks::BaseHook def call(session:, new_messages:) # inspect new_messages, write to session.custom_metadata endendRegistered in config/initializers/transcript_hooks.rb via TranscriptHooks::Registry.
When they run
Section titled “When they run”Three properties worth knowing:
- They run only when new messages are actually broadcast. A poll that finds nothing new runs no hooks.
- They run after the transcript is saved, so a hook can rely on
session.transcriptbeing current. - They’re sequential and error-isolated. One hook raising doesn’t stop the others.
The one that ships
Section titled “The one that ships”GithubPrUrlHook scrapes https://github.com/{owner}/{repo}/pull/{n} out of the transcript and
writes it to session.custom_metadata["github_pull_request_url"].
That one field is load-bearing. It’s what GitHubPullRequestPollerJob (CI status),
GithubCommentPollerJob (review comments), and GitHubMergeConflictPollerJob all key off. If the hook
misses the URL, none of Zimmer’s GitHub integration works for that session — the agent’s PR exists,
but Zimmer doesn’t know about it, so it can’t tell the agent when CI goes red or when someone comments.
Writing one
Section titled “Writing one”module TranscriptHooks class MyHook < BaseHook def call(session:, new_messages:) new_messages.each do |msg| next unless msg["type"] == "tool_result" # ... end session.update_column(:custom_metadata, session.custom_metadata.merge("my_key" => value)) end endendThen register it:
TranscriptHooks::Registry.register(TranscriptHooks::MyHook)What a hook is good for
Section titled “What a hook is good for”The pattern is “derive a structured fact from unstructured agent output, so the rest of Zimmer can act on it.” The PR URL is the canonical example: the agent produces prose and tool output; the hook turns it into a queryable field; three cron jobs then use that field to close the loop between the agent and GitHub.
Anything you want to poll on after the agent mentions it is a good candidate.