Skip to content

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.

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
end
end

Registered in config/initializers/transcript_hooks.rb via TranscriptHooks::Registry.

Three properties worth knowing:

  1. They run only when new messages are actually broadcast. A poll that finds nothing new runs no hooks.
  2. They run after the transcript is saved, so a hook can rely on session.transcript being current.
  3. They’re sequential and error-isolated. One hook raising doesn’t stop the others.

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.

app/services/transcript_hooks/my_hook.rb
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
end
end

Then register it:

config/initializers/transcript_hooks.rb
TranscriptHooks::Registry.register(TranscriptHooks::MyHook)

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.