This document describes every automated workflow implemented in Octobird: event-driven handlers and scheduled tasks. For each workflow the relevant feature flag, trigger, and decision logic are explained. Mermaid diagrams illustrate the more complex flows.
- Handler Overview
- User Commands
- Pull Request Workflows
- Notification Workflows
- Scheduled Tasks
- Configuration Reference
| Handler | Event | Action(s) | Feature Flag | Key Prerequisites |
|---|---|---|---|---|
AssignCommandHandler |
issue_comment |
created |
assign-command |
Issue has difficulty label (GFI/beginner/intermediate/advanced); commenter is not a bot; issue unassigned; commenter is not a committer; includes mentor assignment for GFI newcomers |
UnassignCommandHandler |
issue_comment |
created |
unassign-command |
Comment matches /unassign; issue is open and not a PR; commenter is assigned |
MissingLinkedIssueHandler |
pull_request |
opened, edited, reopened |
missing-linked-issue |
Sender is not a bot; PR is not merged; PR body has no closing reference |
MergeConflictHandler |
pull_request |
opened, synchronize, reopened |
merge-conflict |
Sender is not a bot; PR is not a draft; mergeable state is "dirty" |
VerifiedCommitsHandler |
pull_request |
opened, synchronize |
verified-commits |
Sender is not a bot; at least one commit is unverified |
NextIssueRecommendationHandler |
pull_request |
closed |
next-issue-recommendation |
PR is merged; sender is not a bot; linked issue has beginner/GFI label |
WorkflowFailureNotificationHandler |
workflow_run |
completed |
workflow-failure-notification |
Workflow conclusion is "failure"; affected PRs found |
GfiCandidateNotificationHandler |
issues |
labeled |
gfi-candidate-notification |
Label matches GFI candidate label; teams.gfi-candidate-team is configured |
| Task | Frequency | Feature Flag | Key Prerequisites |
|---|---|---|---|
IssueReminderNoPrTask |
Daily | issue-reminder-no-pr |
Issue assigned for >= issueReminderDays; no linked PR; no recent /working comment |
PrInactivityReminderTask |
Daily | pr-inactivity-reminder |
PR author is not a bot; no commits for >= prInactivityDays |
InactivityUnassignTask |
Daily | inactivity-unassign |
Assignee inactive for >= inactivityDays; no recent /working comment |
LinkedIssueEnforcerTask |
Twice-weekly | linked-issue-enforcer |
PR older than linkedIssueEnforcerDays; no closing reference or author not assigned |
CommunityCallReminderTask |
Bi-weekly | community-call-reminder |
Today matches bi-weekly schedule from anchor date; date not cancelled |
OfficeHoursReminderTask |
Bi-weekly | office-hours-reminder |
Today matches bi-weekly schedule from anchor date; date not cancelled |
All handlers use HTML comment markers to prevent duplicate bot messages.
All commands are triggered by posting a comment on an issue or pull request. The patterns are configurable in
.github/hiero-bot.yml under commands.
Handler: AssignCommandHandler
Feature flag: features.assign-command
Trigger: issue_comment created on any issue with a recognized difficulty label
One handler covers all four difficulty levels. The level is determined by label priority: Advanced > Intermediate > Beginner > GFI.
flowchart TD
A([Comment created]) --> B{Is commenter a bot?}
B -- Yes --> Z([Ignore])
B -- No --> C{Issue has Advanced / Intermediate / Beginner / GFI label?}
C -- None --> Z
C -- Level determined --> D{Contains /assign command?}
D -- No --> Z
D -- Yes --> E{Issue already has assignee?}
E -- Yes --> F[Post: already assigned]
E -- No --> G{Committer of repo ADMIN/WRITE?}
G -- Yes --> H[Post: committers can self-assign]
G -- No --> I{Open assignments ≥ normalUserMax?}
I -- Yes --> J[Post: assignment limit exceeded]
I -- No --> K{On spam list?}
K -- Yes --> L[Post: spam users blocked]
K -- No --> M{Meets level prerequisite?}
M -- No --> N[Post rejection with prerequisite info]
M -- Yes --> O[Assign user + post confirmation]
O --> P{Level is GFI?}
P -- No --> Z2([Done])
P -- Yes --> Q{Mentor marker already present?}
Q -- Yes --> Z2
Q -- No --> R{Assignee has merged PRs?}
R -- Yes --> Z2([Experienced — skip mentor])
R -- No --> S[Load mentor roster]
S --> T{Roster empty?}
T -- Yes --> Z2
T -- No --> U[Post welcome comment with mentor mention + marker]
Level prerequisites (configurable):
| Level | Prerequisite | Default |
|---|---|---|
| GFI | none | — |
| Beginner | ≥ N closed GFI issues | 1 |
| Intermediate | ≥ N closed Beginner issues | 0 (guard inactive) |
| Advanced | ≥ N closed Intermediate issues | 1 |
Config keys: assignment-limits.normal-user-max,
guards.required-gfi-count-for-beginner, guards.required-beginner-count-for-intermediate,
guards.required-intermediate-count-for-advanced
Handler: UnassignCommandHandler
Feature flag: features.unassign-command
Trigger: issue_comment created on any open issue
flowchart TD
A([Comment created]) --> B{Is PR or closed issue?}
B -- Yes --> Z([Ignore])
B -- No --> C{Is commenter a bot?}
C -- Yes --> Z
C -- No --> D{Comment contains /unassign?}
D -- No --> Z
D -- Yes --> E{Commenter is current assignee?}
E -- No --> Z
E -- Yes --> F{Marker already present?}
F -- Yes --> Z
F -- No --> G[Remove user from assignees]
G --> H[Post confirmation comment with marker]
Handler: MissingLinkedIssueHandler
Feature flag: features.missing-linked-issue
Trigger: pull_request opened, edited, or reopened
flowchart TD
A([PR opened / edited / reopened]) --> B{PR author is bot?}
B -- Yes --> Z([Ignore])
B -- No --> C{PR already merged?}
C -- Yes --> Z
C -- No --> D{PR body contains closing reference?\ne.g. Fixes #123}
D -- Yes --> Z
D -- No --> E{Marker already present?}
E -- Yes --> Z
E -- No --> F[Post guidance comment with\nexample formats + marker]
Handler: MergeConflictHandler
Feature flag: features.merge-conflict
Trigger: pull_request opened, synchronized, or reopened
GitHub computes mergeability asynchronously. The handler retries up to 10 times with a 2-second delay when the state is
unknown.
flowchart TD
A([PR opened / sync / reopened]) --> B{PR author is bot or PR is draft?}
B -- Yes --> Z([Ignore])
B -- No --> C[Check PR mergeable state\nretry up to 10x if 'unknown']
C --> D{Mergeable state?}
D -- clean --> Z([No conflict])
D -- unknown after retries --> Z
D -- dirty --> E{Marker already present?}
E -- Yes --> Z
E -- No --> F[Post conflict resolution guide\n+ special advice for CHANGELOG.md + marker]
Handler: VerifiedCommitsHandler
Feature flag: features.verified-commits
Trigger: pull_request opened or synchronized
Fails closed: if pagination is truncated and no unverified commits were found yet, one is assumed unverified.
flowchart TD
A([PR opened / synchronized]) --> B{PR author is bot?}
B -- Yes --> Z([Ignore])
B -- No --> C{Marker already present?}
C -- Yes --> Z
C -- No --> D[Iterate commits, max 500]
D --> E{Any unverified commits found?\nor pagination truncated with 0 found?}
E -- No --> Z([All commits verified])
E -- Yes --> F[Post list of unverified commits\nup to 10 shown + signing instructions + marker]
Handler: NextIssueRecommendationHandler
Feature flag: features.next-issue-recommendation
Trigger: pull_request closed (merged only)
When a contributor merges their first PR on a beginner or GFI issue, the bot suggests up to 5 similar open issues to keep them engaged.
flowchart TD
A([PR closed]) --> B{PR is merged?}
B -- No --> Z([Ignore — just closed])
B -- Yes --> C{PR author is bot?}
C -- Yes --> Z
C -- No --> D{PR body has linked issue?}
D -- No --> Z
D -- Yes --> E{Linked issue has beginner or GFI label?}
E -- No --> Z([Advanced/intermediate — skip])
E -- Yes --> F[Search open unassigned beginner issues\nfallback to GFI issues]
F --> G[Filter out the just-solved issue]
G --> H{Any candidates found?}
H -- No --> Z
H -- Yes --> I[Post recommendation comment\nwith up to 5 issue links]
Handler: GfiCandidateNotificationHandler
Feature flag: features.gfi-candidate-notification
Trigger: issues event with action labeled
flowchart TD
A([Issue labeled]) --> B{Label is GFI candidate label?}
B -- No --> Z([Ignore])
B -- Yes --> C{teams.gfi-candidate-team configured?}
C -- No --> Z
C -- Yes --> D{Marker already present?}
D -- Yes --> Z
D -- No --> E[Post review request comment with team mention + marker]
Config key: teams.gfi-candidate-team (single @org/team or @username mention)
Handler: WorkflowFailureNotificationHandler
Feature flag: features.workflow-failure-notification
Trigger: workflow_run event with action completed
flowchart TD
A([Workflow run completed]) --> B{Conclusion is 'failure'?}
B -- No --> Z([Ignore])
B -- Yes --> C[Collect PR numbers from payload]
C --> D{Any PRs found in payload?}
D -- No --> E[Search PRs by head branch name]
D -- Yes --> F
E --> F{For each PR:\nmarker already present?}
F -- Yes --> Z
F -- No --> G[Post failure notification comment\nwith checks guidance + marker]
Scheduled tasks run periodically across all registered repositories. The scheduler is configured in
ScheduledTaskManager and each task's feature flag must be enabled in the repo config.
Task: IssueReminderNoPrTask
Feature flag: features.issue-reminder-no-pr
Frequency: Daily
Threshold: scheduled.issue-reminder-days (default: 7 days)
flowchart TD
A([Daily run]) --> B[Iterate open issues]
B --> C{Issue is a PR?}
C -- Yes --> B
C -- No --> D{Has assignees?}
D -- No --> B
D -- Yes --> E{Marker already present?}
E -- Yes --> B
E -- No --> F{Any assignee posted /working\nwithin issueReminderDays?}
F -- Yes --> B
F -- No --> G{Days since last assignment >= issueReminderDays?}
G -- No --> B
G -- Yes --> H{Any open PR links to this issue?}
H -- Yes --> B
H -- No --> I[Post reminder comment\nmentioning all assignees]
I --> B
Task: PrInactivityReminderTask
Feature flag: features.pr-inactivity-reminder
Frequency: Daily
Threshold: scheduled.pr-inactivity-days (default: 10 days)
flowchart TD
A([Daily run]) --> B[Iterate open PRs]
B --> C{PR author is bot?}
C -- Yes --> B
C -- No --> D{Days since last commit >= prInactivityDays?}
D -- No --> B
D -- Yes --> E{Marker already present on PR?}
E -- Yes --> B
E -- No --> F[Post inactivity reminder comment with marker]
F --> B
Task: InactivityUnassignTask
Feature flag: features.inactivity-unassign
Frequency: Daily
Threshold: scheduled.inactivity-days (default: 21 days)
This task has two phases. Phase A handles issues without a PR; Phase B handles issues with a stale linked PR.
flowchart TD
A([Daily run]) --> B[Iterate open issues]
B --> C{Is a PR?}
C -- Yes --> B
C -- No --> D{Has assignees?}
D -- No --> B
D -- Yes --> E{For each assignee:\nDays since assignment >= inactivityDays?}
E -- No --> B
E -- Yes --> F{Assignee posted /working\nwithin inactivityDays?}
F -- Yes --> B
F -- No --> G{Any open PR linked to issue?}
G -- No --> H[PHASE A:\nUnassign + post explanation comment]
H --> B
G -- Yes --> I{Days since last commit\non linked PR >= inactivityDays?}
I -- No --> B
I -- Yes --> J[PHASE B:\nClose PR + unassign + post explanation comment]
J --> B
Note:
/workingacts as an immunity token — posting it within the inactivity window resets the timer and prevents this task from acting.
Task: LinkedIssueEnforcerTask
Feature flag: features.linked-issue-enforcer
Frequency: Twice-weekly
Grace period: scheduled.linked-issue-enforcer-days (default: 3 days)
flowchart TD
A([Twice-weekly run]) --> B[Iterate open PRs]
B --> C{PR author is bot?}
C -- Yes --> B
C -- No --> D{PR age < linkedIssueEnforcerDays?}
D -- Yes --> B
D -- No --> E{PR body contains closing reference\ne.g. Fixes #N?}
E -- No --> F[Close PR + comment:\nno linked issue]
F --> B
E -- Yes --> G{requireAuthorAssigned is true?}
G -- No --> B
G -- Yes --> H{PR author is assigned\nto the linked issue?}
H -- Yes --> B
H -- No --> I[Close PR + comment:\nauthor not assigned to issue]
I --> B
Config key: scheduled.require-author-assigned (default: true)
Task: CommunityCallReminderTask
Feature flag: features.community-call-reminder
Frequency: Daily check, posts bi-weekly
The task runs every day but only posts reminders on the scheduled bi-weekly day. The schedule is derived from
scheduled.community-call.anchor-date: reminders are sent on the same day of the week as the anchor, every 14 days from
it.
flowchart TD
A([Daily run]) --> B{anchorDate is configured?}
B -- No --> Z([Disabled])
B -- Yes --> C{Today is same day-of-week as anchor\nAND daysBetween anchor & today divisible by 14\nAND daysBetween >= 0?}
C -- No --> Z
C -- Yes --> D{Today is in cancelledDates?}
D -- Yes --> Z
D -- No --> E[Iterate open issues, collect newest\nopen issue per non-bot, non-excluded author]
E --> F{For each issue:\nmarker already present?}
F -- Yes --> next
F -- No --> G[Post community call reminder\nwith meeting link + marker]
G --> next([Next author])
Config keys: scheduled.community-call.anchor-date, scheduled.community-call.cancelled-dates,
scheduled.community-call.excluded-authors, scheduled.community-call.meeting-link,
scheduled.community-call.calendar-link
Task: OfficeHoursReminderTask
Feature flag: features.office-hours-reminder
Frequency: Daily check, posts bi-weekly
Identical scheduling logic to the Community Call Reminder, but posts on open pull requests instead of issues.
flowchart TD
A([Daily run]) --> B{anchorDate is configured?}
B -- No --> Z([Disabled])
B -- Yes --> C{Today matches office-hours\nbi-weekly schedule?}
C -- No --> Z
C -- Yes --> D{Today is in cancelledDates?}
D -- Yes --> Z
D -- No --> E[Iterate open PRs, collect newest\nopen PR per non-bot, non-excluded author]
E --> F{For each PR:\nmarker already present?}
F -- Yes --> next
F -- No --> G[Post office hours reminder\nwith meeting link + marker]
G --> next([Next author])
Config keys: scheduled.office-hours.anchor-date, scheduled.office-hours.cancelled-dates,
scheduled.office-hours.excluded-authors, scheduled.office-hours.meeting-link, scheduled.office-hours.calendar-link
All settings are read from .github/hiero-bot.yml in each repository. Missing keys fall back to the documented
defaults.
| Key | Default | Controls |
|---|---|---|
assign-command |
true |
/assign command (all difficulty levels, includes limit enforcement and mentor assignment) |
unassign-command |
true |
/unassign command |
missing-linked-issue |
true |
PR linked-issue reminder |
merge-conflict |
true |
Merge conflict detection on PRs |
verified-commits |
true |
GPG-signed commit check on PRs |
next-issue-recommendation |
true |
Suggest next issues on merged beginner PR |
workflow-failure-notification |
true |
Notify on CI failure |
gfi-candidate-notification |
true |
Notify GFI team on candidate label |
inactivity-unassign |
true |
Daily inactivity unassignment |
issue-reminder-no-pr |
true |
Daily reminder for issues without PR |
pr-inactivity-reminder |
true |
Daily stale PR reminder |
linked-issue-enforcer |
true |
Twice-weekly linked-issue enforcement |
community-call-reminder |
true |
Bi-weekly community call reminders |
office-hours-reminder |
true |
Bi-weekly office hours reminders |
| Key | Default | Description |
|---|---|---|
inactivity-days |
21 |
Days after which inactive assignees are removed |
issue-reminder-days |
7 |
Days before "no PR yet" reminder fires |
pr-inactivity-days |
10 |
Days without commits before PR reminder fires |
linked-issue-enforcer-days |
3 |
Grace period before enforcing linked issue |
require-author-assigned |
true |
Also enforce that PR author is assigned to linked issue |
| Key | Default | Description |
|---|---|---|
required-gfi-count-for-beginner |
1 |
GFI issues needed before claiming beginner |
required-beginner-count-for-intermediate |
0 |
Beginner issues needed (0 = guard disabled) |
required-intermediate-count-for-advanced |
1 |
Intermediate issues needed before claiming advanced |
| Key | Default | Description |
|---|---|---|
normal-user-max |
2 |
Max open assignments per user |
| Key | Default | Description |
|---|---|---|
good-first-issue |
"Good First Issue" |
GFI difficulty label |
beginner |
"beginner" |
Beginner difficulty label |
intermediate |
"intermediate" |
Intermediate difficulty label |
advanced |
"advanced" |
Advanced difficulty label |
gfi-candidate |
"good first issue candidate" |
GFI review candidate label |
| Key | Default | Description |
|---|---|---|
gfi-candidate-team |
"" |
@org/team mention for GFI candidate review |
| Key | Default regex | Description |
|---|---|---|
assign-pattern |
/assign\b |
Matches /assign command |
unassign-pattern |
`(^ | \s)/unassign(\s |
working-pattern |
`(^ | \s)/working(\s |
Every handler that posts a comment uses a unique HTML comment marker (e.g. <!-- hiero-bot:inactivity-unassign -->)
embedded in the comment body. Before posting, the handler checks whether the marker is already present on the issue or
PR. This prevents duplicate bot messages if the trigger fires multiple times.
The marker strings are configurable under the markers YAML key, but the defaults are stable and do not need to be
changed in normal operation.