Skip to main content
The handoff example gives your team one agent to throw work at. Mention it on a pull request, in a review comment, or in Slack, and it takes ownership: it binds the PR to its session, fixes blockers, reacts to CI failures and review feedback, repairs merge conflicts, and tags the original human when the PR is ready for final review. It is the general-purpose “own this until it’s done” agent, and the example that exercises session bindings most completely.

How it works

  1. A mention starts a session. Someone writes @auto.handoff in a PR comment, a review, or a Slack message. The mention arrives as a github.* or chat.message.mentioned event, matches a spawn-routed trigger, and a fresh session starts with the trigger context rendered into its prompt.
  2. The session claims the PR. After identifying (or opening) the pull request, the agent calls the auto.bind tool with target type github.pull_request. From then on the session owns that PR as a binding target.
  3. Every later event routes home. Follow-up comments, reviews, failing checks, aggregate CI success, and merge conflicts on that PR all match bind-routed triggers, which resolve the PR’s binding and deliver into the owning session instead of spawning strangers.
  4. The session waits by ending. After pushing a fix or acknowledging feedback, the agent leaves a status update and ends its turn. It never sleeps or polls — the next event wakes it. A session can span a PR’s entire life this way.
  5. A human closes the loop. The agent declares the PR ready only after CI is green and review feedback is addressed, and it merges only when a human explicitly asks.

The configuration

Two files: a reusable runtime fragment and the agent itself. The agent file is long because the system prompt carries the entire ownership protocol — in auto, process lives in prompts, not in code.
.auto/fragments/environments/agent-runtime.yaml
.auto/agents/handoff.yaml
The example assumes two connections: a GitHub connection named github-acme and a Slack connection named slack. It also references an avatar at .auto/assets/handoff.png — drop any square PNG or JPEG (512–2000 px, up to 2 MiB) at that path, or remove the avatar block.

Walkthrough

Nine triggers, three routing kinds

The trigger list looks long, but it is one decision table: who should hear about this event? Every mention and conversation trigger filters $.github.auto.authored: false (and chat triggers $.auto.authored: false): the agent’s own comments echo back as events, and this is the standing convention that keeps it from replying to itself. The check and merge-conflict triggers need no such filter — those events are emitted by CI and by the platform, not by agent comments. See triggers for the full where grammar.

changedTo catches mentions added by edit

The second trigger fires when an edit turns a mention on:
changedTo matches only when the value now equals true and the payload’s sibling previous object shows it was different before. GitHub events carry github.auto.previous.mentioned for exactly this purpose, so editing a comment that already mentioned the agent does not re-spawn a session — only the edit that first introduces the mention does.

Bindings make one session own the PR

The spawn triggers create the session; the auto.bind call in the prompt gives it continuity. Once the session binds github.pull_request for PR #212, every bind-routed trigger resolves that binding and delivers into the same session — conversation, checks, conflicts. onUnmatched: drop says events about PRs nobody owns are discarded silently rather than spawning sessions for every comment in the repository. Two check-trigger details are worth stealing for any CI-reactive agent:
  • Aggregate vs. individual checks. The failure trigger excludes the roll-up check (name: notIn: [All checks]) so the agent reacts to the specific failing job; the success trigger matches only the roll-up (name: All checks), so “everything is green” arrives exactly once. Substitute your repository’s actual roll-up check name.
  • Stale heads are skipped. $.github.checkRun.headIsCurrent: { notIn: [false] } ignores check results for commits that a newer push has already superseded.
github.check_run.completed deliveries are deferred until the session is idle instead of interrupting the current turn — a burst of check results will not derail a fix in progress. Check events also require a PR association; checks on non-PR commits never become events.

The Slack side: attributions split new work from steering

The two Slack message triggers cover the same chat.message.mentioned event with mutually exclusive filters on $.auto.attributions — the payload field linking a thread to the sessions already working it:
  • exists: false → nobody owns this thread yet → spawn a handoff.
  • exists: true → this thread already has attributed sessions → deliver to them.
This split is enforced at apply time: a spawn trigger and an attributedSessions deliver trigger on the same chat event must declare exactly these opposing filters, and every attributedSessions deliver on chat messages must filter $.auto.authored: false. The prompt’s auto.chat.subscribe call is what attributes the #dev thread to the session in the first place (it is an alias of auto.bind with a slack.thread target).

Write access is scoped per capability

The mount’s githubApp auth declares exactly what the session’s Git credential and brokered GitHub tools may do: contents, pullRequests, issues, and workflows writes, checks and actions reads, and — deliberately — merge: write. Merge is a separate capability because merging to the default branch is a real escalation beyond opening PRs: merge_pull_request stays invisible and uncallable unless the mount grants it, even though it is named in the tools list, and it is excluded from the default tool allowlist. Granting it here is what lets a human say “merge it” in the thread and have the agent comply; drop the capability and the tool disappears while everything else keeps working. commitAuthor sets the name and email on commits the session pushes. Tokens for all of this are minted server-side and short-lived: GitHub tool calls run through a server-side proxy, and git pushes fetch fresh credentials from the platform’s credential broker at use time, so no long-lived credential lives in the sandbox.

Install the packaged Shepherd

The supported managed counterpart to this standalone Handoff example is Shepherd. Instead of copying the files above, install it from the consolidated agent package:
.auto/agents/shepherd.yaml
This import creates the Shepherd resource, which users address as @auto.shepherd. It does not preserve the retired @auto/handoff package as an alias or create a separate @auto.handoff custom resource. Shepherd is GitHub-first — handoffs arrive through PR and issue mentions, and Slack acknowledgement is optional via a standard connection named slack — so its behavior is a superset of the directory above. Under GitHub Sync, repoFullName and githubConnection default from the Sync binding, so the variables block can often be omitted entirely. Override any inherited field by declaring it in the importing file (your fields merge last), and drop inherited entries with remove: { triggers: [...], tools: [...] }. See managed templates and imports and fragments.

Adapt it

  • Replace acme/widgets, github-acme, slack, and #dev with your repository, connection names, and channel.
  • Set the aggregate check name (All checks here) to your repository’s real roll-up check, or delete that filter to react to every check individually.
  • Point the system prompt at your real setup docs, test commands, and branch policy — the ownership protocol is only as good as what it names.
  • Remove merge: write from the mount and merge_pull_request from the tools if merging must stay entirely human.
  • Keep the accidental-handoff paragraph. Documentation and examples quote agent handles constantly; an agent that takes ownership every time it is quoted is a nuisance.
  • This example pairs naturally with code review: the handoff agent’s readiness gate waits for the reviewer’s verdict on the latest commit. Without a reviewer agent, replace that clause with your team’s review requirement.

Try it

Merge the PR that installs the packaged Shepherd and let GitHub Sync apply it. Then comment on any small PR:
Watch the session spawn in the web app, and confirm it acknowledges on the PR, binds it, pushes a fix, and comes back when CI turns green. Then reply to one of its comments — the reply should land in the same session, not a new one. If you copied the standalone example instead and kept its name: handoff, use @auto.handoff for that custom resource.