The triggers: config key lets an operator declare CEL (Common Expression
Language) rules that decide when an
agent should be kicked for an incoming event — issue opened, issue labeled,
PR opened, a comment posted, and so on — without writing code.
triggers:
- name: bug-triage
expr: event.kind == "issue.opened" && hasLabel(event.labels, "bug")
agent: triager
priority: 10This is implemented by pkg/celtrigger.
triggers: is additive. It runs alongside — not in place of — hive's
built-in label/governor triggering: the governor's eval cycle evaluates the
compiled rule set against each enumerated actionable item and unions the
matched agent names into the due-agents set, after its own pause/budget/
on-demand gates already apply. An empty (or absent) triggers: list compiles
to a valid engine that never matches anything, so leaving it out is
byte-identical to today's behavior — nothing about default triggering
changes because this feature exists.
Each entry under triggers: is one TriggerRule (pkg/config/config.go):
| YAML field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes (in practice) | Human-readable identifier for logs/diagnostics. Not enforced non-empty by the schema, but an empty name is rendered as rule[<index>] in error messages, so name your rules. |
expr |
string | yes | The CEL expression. Must compile and must evaluate to a bool. An empty expression is a compile-time error. |
agent |
string | yes | The agent to trigger when expr evaluates true. A rule with an empty agent compiles fine but is silently skipped when collecting matched agents. |
priority |
int | no (default 0) |
Orders competing rules when several match the same event — higher sorts first. Ties preserve declaration order. |
Source: TriggerRule struct, pkg/config/config.go (Name, Expr, Agent,
Priority fields with their yaml:"..." tags), and pkg/celtrigger/wire.go's
CompileFromConfig, which maps each TriggerRule 1:1 onto a
celtrigger.Rule.
Every rule is evaluated against exactly one variable: event, a
celtrigger.NormalizedEvent — hive's forge-neutral representation of a
source-control event. event is the only activation exposed to the
expression; there is no other variable, no access to config, no access to
agent state.
The reachable fields, taken from the cel:"..." struct tags on
NormalizedEvent (pkg/celtrigger/celtrigger.go), are:
| CEL field | Go type | Meaning |
|---|---|---|
event.kind |
string | Event kind — see the Kind* constants below. |
event.repo |
string | Repository spelling from project.repos, e.g. "name" or "org/name"; use the configured spelling, not necessarily the qualified display label. |
event.labels |
list of string | Labels on the issue/PR. |
event.title |
string | Issue/PR title. |
event.author |
string | Login/handle of the actor that produced the event. |
event.body |
string | Issue/PR/comment body. |
event.is_draft |
bool | Whether a PR is a draft. |
event.number |
int | Issue/PR number. |
event.state |
string | Current state, e.g. "open"/"closed". |
event.base_branch |
string | Target branch for a PR. |
event.head_branch |
string | Source branch for a PR. |
event.assignees |
list of string | Currently-assigned logins. |
event.comment |
string | Body of the triggering comment (for comment.created). |
Field access is type-checked at compile time against this registered native
type — an expression that references a field not in this table (e.g.
event.does_not_exist) is rejected at Compile, not left to fail silently
at runtime.
event.kind takes one of these forge-neutral values (pkg/celtrigger/celtrigger.go):
issue.opened
issue.labeled
issue.closed
issue.reopened
pr.opened
pr.labeled
pr.ready_for_review
pr.closed
pr.merged
comment.created
issue.opened and pr.opened are edge-triggered: the eval cycle emits them
once per issue/PR (keyed repo#number) per hive process, the first time the
item is enumerated, not on every cycle while it stays actionable. The seen-set
is in memory, so a hive restart re-emits currently actionable items once.
Open Settings → Features → Work intake & issue lifecycle → Event triggers, or use the Event triggers button in Agent → Configuration → Channels. Owner access is required. Choose Run scanner when an issue is first seen, enter a rule name, select the existing scanner agent and one managed repository, then use Save Event trigger. No YAML editing is required. The repository picker displays qualified labels but saves the exact configured repository spelling used by evaluation; there is no all-repositories default.
Existing rules can be edited with Advanced CEL, preserving their expression,
order, target and priority unless explicitly changed. Higher priority sorts
first; ties retain declaration order. There is no enabled field: remove a rule
to stop its routing. These operations do not change unrelated settings or
default governor behavior.
The editor validates the complete rule list, including CEL compilation and a boolean result, before persistence. New or edited rules must name an existing agent. Save errors keep the draft visible; a revision conflict requires reloading the latest rules instead of overwriting another operator's changes. Reload (discard draft) explicitly discards the pending edit. This editor has its own save button; the dialog's Save & close does not save its draft. Successful saves are durable and apply on the next event evaluation, with no restart required; saving does not immediately kick an agent. Updating rules does not reset already-seen item history.
Agent → Configuration → Channels is not a GitHub webhook installer.
Only the governor kick channel has a supported runtime; the old webhook,
discord, schedule, and bead channel runtimes were removed.
For file-based configuration, the equivalent is a top-level rule in
hive.yaml (keep existing rules):
triggers:
- name: scanner-new-issue
expr: event.kind == "issue.opened" && event.repo == "org/repo"
agent: scannerReplace org/repo with the exact spelling from project.repos and ensure the
scanner is enabled. Normal pause, budget, and on-demand gates still apply.
This means first seen actionable issue, not exclusively an issue created
after the rule was installed: existing actionable issues match once, and
restart replays them once as described above.
For prompt delivery rather than waiting for reconciliation, use the existing
signed GitHub webhook receiver and subscribe to issues:
operator setup.
Hosted hives normally receive the GitHub App's signed deliveries through the
hub relay; stand-alone hives use POST /api/webhook/github.
The receiver requires GITHUB_WEBHOOK_SECRET and a matching HMAC signature.
Saving a channel or adding a CEL rule neither registers a webhook nor
configures its secret. Delivery wakes an early governor evaluation subject
to debounce/minimum-gap pacing; it does not bypass agent execution gates.
Besides the standard CEL string/list operators (in, .contains(...),
.startsWith(...), .matches(...), &&, ||, !, ==, comprehensions
like .exists(...)/.all(...)), one custom helper is registered:
hasLabel(event.labels, "bug")— convenience for"bug" in event.labels; returnsfalse(not an error) for non-list or non-string arguments.
No other custom functions exist. Do not invent one — an expression calling an unregistered function is a compile-time error.
Each rule's evaluation is bounded (maxEvalCost = 10_000 in
pkg/celtrigger/celtrigger.go) to stop pathological nested comprehensions
(e.g. a triple-nested .all() over labels) from burning runtime per event.
A rule that exceeds the budget is treated as no-match for that
evaluation (not an error, not a crash) and logs a warning naming the rule
and the budget so you can diagnose why it stopped firing. Realistic
operator rules — field checks, string predicates, a label
.exists(...)/.contains(...) — comfortably fit inside the budget.
This is a safety property, stated plainly:
- Compile time (config load): every rule's
expris parsed, type-checked, and confirmed to returnbool. If any rule intriggers:is malformed — a syntax error, a reference to a field that doesn't exist, a call to an unknown function, an expression that returns something other thanbool, or an empty expression — the entire config load fails with an error. No partial engine is produced; the bad rule cannot reach a running fleet. - Runtime (event evaluation): if a compiled rule still errors while evaluating a specific event (including exceeding the cost budget above), that is treated as "no match," not a crash and not an error propagated to the caller. One misbehaving rule cannot take down evaluation for the other rules or for the event pipeline.
In short: a bad rule is caught early and loudly (at config load); a rule that merely fails to match at runtime does so quietly and safely.
This rule set compiles and matches, verified against
pkg/celtrigger/wire_test.go's TestCompileFromConfig_ValidAndMatch:
triggers:
- name: bug
expr: hasLabel(event.labels, "bug")
agent: triager
priority: 10
- name: pr
expr: event.kind == "pr.opened"
agent: reviewer
priority: 5An issue.labeled event carrying the label bug matches the bug rule and
kicks triager. A more selective example, combining a kind check, a
non-draft check, and a target-branch check — verified against
pkg/celtrigger/celtrigger_test.go's TestMatch_BooleanCombos:
triggers:
- name: ready-pr
expr: event.kind == "pr.opened" && !event.is_draft && event.base_branch == "main"
agent: reviewerThis matches a non-draft PR opened against main, and does not match a
draft PR against the same branch.
A slash-command-style comment trigger — verified against
TestMatch_CommentEvent:
triggers:
- name: slash-cmd
expr: event.kind == "comment.created" && event.comment.startsWith("/hive")
agent: cmdState-triggered hooks use the same CEL engine and the same
fail-closed posture for their optional when: predicate, but bind a
different, unrelated variable (t, the transition payload) — see
Predicates (when:) in that doc. triggers:
and hooks' when: are two separate surfaces that happen to share an
evaluation engine: triggers: decides whether to kick an agent for a
normalized source-control event, while hooks' when: decides whether to
fire an action on a state transition. Don't mix event.* fields into a
hook's when: or t.* fields into a triggers: rule — the field sets are
disjoint and referencing the wrong one is a compile error.