Skip to content

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.

Polls a channel for new_message or bot_mention. Optionally scoped to a thread (thread_ts) and an allowlist of user IDs.

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.

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.

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 external is polled. There are no webhooks anywhere in Zimmer.

JobCadence
SlackTriggerPollerJobevery minute
ScheduleTriggerJobevery minute
GitHubPullRequestPollerJobevery 30 seconds
GithubCommentPollerJobevery 30 seconds
GitHubMergeConflictPollerJobevery 2 minutes
SlackTriggerHealthCheckJobhourly at :45
CleanupStaleTriggersJobreaps leftovers