By the end of this chapter you can choose reactive and proactive on: events for the Repo Assistant and use gh-aw's admission controls to reduce redundant work without mistaking them for a concurrency lock or a spending cap.
This chapter targets the inspected gh aw v0.88.7. We keep evolving the same Repo Assistant from Chapter 3 — this time teaching it to wake up both on a new issue and on a daily sweep.
In Chapter 1 we framed gh-aw as automation for the repository's outer loop — the judgment work that happens around the edges of writing code. But an outer-loop teammate is only useful if it shows up at the right time. A triager who reads issues a week late is worse than none. The trigger is the workflow's clock: it decides the precise moment the agent is worth spending money and attention on.
Two shapes of “the right moment”
Almost every useful trigger is one of two shapes:
Reactive — something happened, respond now. An issue was opened, a PR was pushed, someone left a comment. The event carries a payload (the issue, the PR) that is the work.
Proactive — on a rhythm, go look for work. A nightly sweep for stale issues, a weekly docs audit. Nothing “happened”; the schedule itself is the prompt.
Great agentic teammates use both. A human maintainer answers issues as they arrive and does a Friday-afternoon cleanup; the Repo Assistant should too. The rest of this chapter is about expressing those two shapes precisely — and about the fact that choosing a trigger is also a security and cost decision, because it decides who and what can make your agent run.
Admission is not enforcement of a resource ceiling. A trigger creates an opportunity to work; admission checks decide whether the agent should take it. A recent sweep or another layer of the same PR stack may make another pass unnecessary. The cooldown and stack filters below implement that idea. They do not reserve credits or serialize competing runs: review concurrency separately, and use the scoped budgets in Chapter 13 for spending controls.
Triggers live in the on: block of the frontmatter. gh-aw builds on standard GitHub Actions events with enhancements for reactions, cost control, and filtering (v0.88.7 Triggers). The simplest reactive form is ordinary Actions syntax:
Trigger excerpt from examples/ch04/repo-assistant-triggers.md — the complete workflow appears below
on:
issues:
types: [opened, reopened]
The everyday events
You'll reach for a small set of triggers constantly. Each one hands the agent a different payload to reason about:
Trigger
Fires when…
Typical use
issues:
an issue is opened, edited, labeled, closed…
triage, auto-response
pull_request:
a PR is opened, synchronized, labeled…
review, CI-doctor
pull_request_review:
a PR review is submitted, edited, or dismissed
review follow-up
issue_comment:
someone comments on an issue or PR
ChatOps, follow-ups
schedule:
a recurring time arrives
sweeps, audits, reports
workflow_dispatch:
you run it manually (UI, API, or gh aw run)
testing, on-demand tasks
workflow_run:
another workflow (e.g. CI) completes
react to build failures
For a pull_request event, or a comment on a PR, the coding agent can access both the PR branch and the default branch — the context it needs to review the change (trigger context).
Human-friendly schedules
For proactive work, gh-aw improves on raw cron. Human-friendly expressions compile to cron; fuzzy scheduling scatters those cron times to reduce load spikes (v0.88.7 Schedule Syntax):
Schedule excerpt from examples/ch04/repo-assistant-triggers.md — daily, not necessarily at night
on:
schedule: daily
The schedule reference also covers preferred-time windows, business-hour windows, and fixed cron. Choose a window when the time of day matters; daily alone does not promise a nightly run.
Scattering is repository-aware: the compiler uses a repository seed as well as the workflow's repository-relative identity. The CLI normally obtains the repository slug from the Git remote; --schedule-seed explicitly overrides it. With the same inputs, recompilation keeps the scattered cron stable; copying a workflow into a different repository or changing its identity can change the result. This distributes load; it does not guarantee collision-free times or punctual execution by GitHub Actions (per-file schedule context; compiler configuration).
Shorthands: the one-line trigger
Many triggers have a natural-language shorthand that expands into Actions syntax and automatically includes workflow_dispatch. A separate, one-comment triager in examples/ch04/repo-assistant-issue-shorthand.md demonstrates the reactive form:
Trigger excerpt from examples/ch04/repo-assistant-issue-shorthand.md — an alternative triager, not the daily-sweep recipe
on: issue opened
Other shorthands cover label matching, path-filtered PR events, pushes to a branch, and fuzzy daily schedules. Treat them as alternatives for the on: field, not several on: keys in one YAML mapping (trigger shorthand reference).
For an explicit human request, the preferred key is on.slash_command, with an underscore; its command name has no leading slash. The slash belongs in the user's comment. The shorter on: /triage form is also supported, while on.command is a deprecated alias, not the spelling to teach in new workflows (v0.88.7 schema; command reference).
Feedback and cost controls attached to the trigger
These enhancements implement the clock's feedback and lifetime controls in the same on: block:
reaction: adds an emoji to a triggering issue, PR, comment, or discussion so a human sees the workflow noticed. A schedule has no such triggering item (reactions).
stop-after: gives an experiment a deadline rather than an indefinite lifetime. For the literal "+30d" used below, a fresh compile with no existing lock resolves a time 30 days ahead. Ordinary recompilation preserves an existing stop time. Deliberately renew a relative deadline with gh aw compile's --refresh-stop-time flag.
A fresh lock with no time to preserve is a different case, not evidence that every recompile renews the deadline. This corrects the older explanation; do not read it as guaranteed suppression of every dispatch path (preservation/refresh contract; compile flags).
Cooldown: admit work less often than events arrive
For admission control, the new on.cooldown lets a frequent schedule check for eligibility without starting the agent every time. It takes a literal Go duration of at least 5m, such as 1h30m or 4h, not a GitHub Actions expression. The separate maintenance-digest example uses this trigger block:
Trigger excerpt from examples/ch04/repo-assistant-cooldown.md — cooldown applies to both the schedule and manual dispatch
on:
schedule: hourly
workflow_dispatch:
cooldown: 4h
stop-after: "+30d"
The check measures from the completion of the latest completed workflow run whose agent job started. Failure counts too: an agent that started and then failed still consumed work. A run whose agent was skipped does not reset the interval. The compiler gives the pre-activation job actions: read so it can inspect that history (cooldown reference; cooldown implementation change).
History lookup failure fails open. If history cannot be queried, the cooldown check allows execution to proceed, subject to other gates. It is a best-effort noise/cost control, not a lock, a reservation, or a hard spend cap. It skips ineligible agent executions rather than holding each event for a later retry.
For a hypothetical example, if an agent-started run finishes at 10:20 UTC, a four-hour cooldown remains active until 14:20 UTC even if that run failed. A skipped agent at 11:20 does not move that time. Passing the check after 14:20 is eligibility, not a promise that GitHub Actions launches the agent then. No scheduler run is being reported here.
Stacked PRs: avoid reviewing the same work at every layer
A stack is a chain of PRs in which each targets the previous one. Reviewing every layer can repeat the same judgment work — another admission problem. In v0.88.7, both on.pull_request.max-stack and on.pull_request_review.max-stack default to 1: the top-most PR only.
A positive integer N admits the top N layers; -1 disables only stack filtering. Non-stacked PRs are unaffected. Fork, role, branch, and other applicable checks still apply when stack filtering is disabled (v0.88.7 stack filtering).
Choosing a trigger is mostly about matching the two shapes from the concept — but a few defaults exist specifically to stop a trigger from becoming an attack vector. These are the parts a reviewer should always check.
Safe defaults you get for free
Forks are blocked by default. Pull request workflows admit same-repository PRs by default; opt specific forks in with on.pull_request.forks. This is a front-line check against untrusted PRs reaching your agent (fork filtering).
Who can trigger is an allowlist. Unsafe triggers (push, issues, pull_request) automatically enforce permission checks. on.roles defaults to [admin, maintainer, write]. Keep rolesinside on, not at the frontmatter's top level. Matching is exact, not a minimum privilege threshold: listing only write does not also admit admins. Failed checks cancel the workflow with a warning (role filtering).
Stacked PRs default to the top layer. If a lower-stack PR appears not to wake the agent, inspect the max-stack admission rule before widening permissions.
workflow_run is hardened. Name at least one upstream workflow in workflows and scope branches; omitting branches warns, or errors in strict mode. The compiler adds repository-ID and fork checks. At this target, on.workflow_run.conclusion also accepts [failure] for CI-failure-only admission. This is v0.88.7-valid syntax, not a filter proven on v0.81.6 — the baseline compiler rejected it (workflow-run reference). The CI-doctor pattern returns in Chapter 10.
A quick decision guide
You want to…
Reach for
respond to each new issue/PR
issues: / pull_request: with types:
do periodic maintenance
schedule: (prefer fuzzy daily/weekly)
let humans invoke on demand
workflow_dispatch:
answer a /command in a comment
on.slash_command (command name without the slash)
react to CI results
workflow_run: with workflows, branches, and an appropriate conclusion filter
trigger from an external system (Jira, PagerDuty)
repository_dispatch:
When not to
Don't trigger on high-frequency events without a filter. Pushes to a busy repo, or every issue_comment, create many opportunities for paid agent work. Filter by label, path, or an explicit slash command so the agent runs only when it's genuinely wanted.
Don't put a global cooldown on urgent triage by accident.on.cooldown covers the workflow's triggers, not just schedule. Adding four hours to the combined issue-and-sweep recipe could suppress a new issue's triage after a sweep or another issue run. The skipped event is not a reservation for later. Keep urgent reactive work separate from a cooled-down maintenance workflow.
Don't open the fork gate casually. Allowing all forks through the fork filter does not remove other admission checks, but it still widens the risk surface. Use the narrowest pattern that meets the need, and pair it with the security model in Chapter 7.
Don't mistake a quieter workflow for a capped bill. Choose a deliberate lifetime with stop-after, inspect concurrency separately, and retain the scoped budgets in Chapter 13. Cooldown fails open on history lookup failure; refreshing a stop time is an explicit policy decision, not routine recompilation.
Let's give the Repo Assistant both shapes at once: it triages new or reopened issues reactively and runs a daily stale-issue sweep proactively. The existing trigger configuration stays intact, with no global cooldown. The Markdown body selects its job from github.event_name.
examples/ch04/repo-assistant-triggers.md — complete workflow: reactive triage plus a proactive daily sweep
---
on:
issues:
types: [opened, reopened]
schedule: daily
workflow_dispatch:
reaction: eyes
stop-after: "+30d"
permissions:
contents: read
issues: read
engine: copilot
network: defaults
safe-outputs:
add-comment:
max: 1
add-labels:
allowed: [bug, enhancement, question, documentation, needs-info, stale]
max: 3
---
# Repo Assistant — triage on open, sweep on a schedule
You are the **Repo Assistant**. This workflow wakes up in two different ways, and
your job depends on which one fired. Check `${{ github.event_name }}` first.
## If an issue was just opened or reopened (`issues`)
A single issue triggered this run. Read its title and body, then:
1. Post **one** short, friendly triage comment that restates the request in a
sentence and names any missing information the reporter should add.
2. Apply the single best-matching label from the allowed set.
## If this is the daily schedule (`schedule`) or a manual run (`workflow_dispatch`)
No single issue triggered this run — you are doing a **daily sweep**. Look at the
open issues that have seen no activity in the last 30 days and, for the few most
clearly abandoned, add the `stale` label and a gentle comment asking whether the
issue is still relevant. Be conservative: when in doubt, leave the issue alone.
This example demonstrates **triggers**: the same Repo Assistant responds to a
per-issue event *and* runs on a recurring `daily` schedule, reacts with :eyes: on
the triggering item, and sets a relative 30-day stop deadline. A fresh compile
with no existing lock resolves that deadline; ordinary recompilation preserves
the existing stop time. Renew it deliberately with `--refresh-stop-time`, not by
assuming every compile extends it.
Read the on: block as the assistant's clock. It wakes up three ways — a new/reopened issue, a fuzzy daily schedule, or a manual dispatch. The reaction applies when there is a triggering item; the relative deadline follows the preserve-versus-refresh rule. Everything else is the posture from earlier chapters: read-only agent permissions, Copilot, curated network access, and proposed writes routed through Chapter 6's safe outputs. The configured limit is one comment per run, not one comment for every issue found in a sweep.
A separate maintenance digest with cooldown
This small companion implements the less-frequent admission policy without slowing the reactive triager. It checks eligibility hourly and permits at most one new digest issue per admitted run. Its manual dispatch is subject to the same cooldown. It is a write-capable report recipe, not a no-write diagnostic probe.
examples/ch04/repo-assistant-cooldown.md — complete, separate workflow: a bounded maintenance digest with a four-hour cooldown
---
on:
schedule: hourly
workflow_dispatch:
cooldown: 4h
stop-after: "+30d"
permissions:
contents: read
issues: read
engine: copilot
network: defaults
safe-outputs:
create-issue:
title-prefix: "[maintenance-digest] "
max: 1
---
# Repo Assistant — maintenance digest with cooldown
You are the **Repo Assistant** on a proactive maintenance pass. This separate
workflow demonstrates `on.cooldown`: scheduled and manual runs share a
four-hour admission interval. It does not handle urgent issue-open events.
Read open issues in this repository and look for issues with no activity in the
last 30 days that clearly need a maintainer's decision. Ignore existing
maintenance-digest issues as candidates.
If there are actionable candidates not already covered by an open
maintenance-digest issue, request **at most one** new issue summarizing them.
Use a title beginning with `[maintenance-digest] `, link to each candidate,
and suggest a next step for a human. Do not close, label, or comment on the
candidate issues. If there is nothing new to report, report no work.
Cooldown is best-effort admission, not a concurrency lock or a spending cap.
History lookup failure fails open. The relative stop deadline is preserved
on ordinary recompilation; renewing it requires `--refresh-stop-time`.
The explicit create-issue output bounds the report to one issue per run in this repository (target safe-output reference). The prompt asks the agent to avoid duplicate digests; that request is not an atomic duplicate-prevention mechanism, just as cooldown is not a reservation.
Compile the sources, then inspect the generated gates
Use the v0.88.7 compiler selected in Chapter 2, and check its version first. Compile copies in a scratch Git repository: compilation emits adjacent locks and can write dependency caches or resolve network-backed data. It invokes no AI engine and needs no engine credential, but it is not universally offline (target compilation process).
Compile-time checks for all three source workflows — commands, not a captured success transcript
gh aw version
gh aw compile examples/ch04/repo-assistant-triggers.md --strict
gh aw compile examples/ch04/repo-assistant-issue-shorthand.md --strict
gh aw compile examples/ch04/repo-assistant-cooldown.md --strict
Only when you deliberately want to renew the combined assistant's relative deadline, recompile with the explicit refresh flag:
Intentional deadline renewal, not the ordinary compile step
gh aw compile examples/ch04/repo-assistant-triggers.md --strict --refresh-stop-time
Live-run boundary. These are compile-time exercises, not scheduler tests; no hourly timing, cooldown failure path, or stack admission has been exercised live. Execution needs configured Copilot authentication and an Issues-enabled repository; the triager's allowed labels must already exist. The book repository has Issues disabled, so reference-context compilation does not certify issue-output deployment to it. The additional --validate checks can depend on repository context and network access (authentication; repository-feature validation).
You can now choose when a workflow should wake up and when its agent should be admitted:
Triggers are the outer loop's clock, and they come in two shapes: reactive (issues, PRs, comments) and proactive (schedules).
The on: block adds repository-aware fuzzy schedules, one-line shorthands, and reaction feedback to standard Actions events. Ordinary recompilation preserves an existing stop deadline; --refresh-stop-time explicitly renews a relative one.
on.cooldown measures from a completed run whose agent started, including failure; skipped agents do not reset it. History lookup failure fails open. It is not a lock, reservation, or hard spend cap.
Choosing a trigger is a security and cost decision. Fork checks, nested on.roles, hardened workflow_run, and top-of-stack defaults decide admission; disabling stack filtering does not disable the other checks.
Keep urgent reactive triage separate from cooled-down maintenance, and review concurrency and scoped budgets independently.
What's next. The assistant now wakes at the right time — but which brain does it think with? In Chapter 5: Engines, we choose and configure the engine (Copilot, Claude, Codex, Gemini, or Pi) and see the portability that engine-neutral design buys you.