Triggers and schedules
A trigger is a session template plus one or more conditions. When any condition fires, the trigger creates a new session — or resumes an existing one.
Conditions on a trigger are ORed. Any one firing fires the trigger.
The three condition types
Section titled “The three condition types”Polls a channel for new_message or bot_mention. Optionally scoped to a thread (thread_ts)
and an allowlist of user IDs.
schedule
Section titled “schedule”Either recurring (interval + unit, or time + day_of_week + timezone) or one-time
(scheduled_at). ScheduleTriggerJob runs every minute — GoodJob/fugit can’t do sub-minute
cron, so a schedule is minute-resolution at best.
ao_event
Section titled “ao_event”Fires when a watched session transitions to session_needs_input, session_failed, or
session_archived. Enqueued directly from the state machine’s pause / fail / archive
callbacks (deferred via after_all_transactions_commit, so the row is visible to the job).
With watched_session_id it’s session-scoped and one-shot. Without it, it’s a broadcast, and
it only fires for is_autonomous sessions.
Wake-up semantics
Section titled “Wake-up semantics”Triggers are the backing store for two MCP tools Zimmer gives its own agents: “wake me up later” and “wake me up when that other session changes state.” Two mechanisms make this reliable:
Auto-sleep. Trigger#sleep_target_session_if_applicable runs on trigger creation. If the
target session is needs_input, it sleeps immediately (needs_input → waiting). If it’s
running, it sets metadata["pending_sleep"] = true and the sleep happens on the next pause.
So an agent can say “wake me in an hour” mid-turn without stranding itself.
Immediate fire on already-matched state. Trigger#fire_ao_event_immediately_if_state_matches
row-locks each watched session inside the creation transaction and enqueues the job immediately
if the watched session is already in the target state. This closes the footgun where you
register a watcher after the transition already happened and then sleep forever.
Sibling cleanup. The recommended pattern is to register three ao_event watchers
(needs_input, failed, archived) plus a wake_me_up_later deadline backstop — whichever
fires first wins. After a successful one-time fire, destroy_sibling_wakes! deletes the others
pointing at the same session. Unless the follow-up was dropped, in which case siblings are
preserved.
Loop prevention. A session whose metadata["trigger_id"] equals the trigger will never
re-fire that trigger.
Everything is polled
Section titled “Everything is polled”Everything external is polled. There are no webhooks anywhere in Zimmer.
| Job | Cadence |
|---|---|
SlackTriggerPollerJob | every minute |
ScheduleTriggerJob | every minute |
GitHubPullRequestPollerJob | every 30 seconds |
GithubCommentPollerJob | every 30 seconds |
GitHubMergeConflictPollerJob | every 2 minutes |
SlackTriggerHealthCheckJob | hourly at :45 |
CleanupStaleTriggersJob | reaps leftovers |