label-driven-state-machine
flowchart TD
Issue([Issue created]) --> Validator[Validate workflow]
Validator -- applies label ready-for-dev --> Generator[Generate workflow]
Generator -- opens PR applies label needs-review --> Reviewer[Review workflow]
Reviewer -- applies label changes-requested --> Fixer[Auto-fix workflow]
Fixer -- re-applies label changes-requested --> Fixer
Fixer -- applies label review-approved --> Human([Human merge decision])
Fixer -- attempt gt 3 --> Escalate([Escalate to human])
Reviewer -- applies label review-approved --> Human
External system labels are used as state tokens in an asynchronous pipeline. Each workflow is triggered by a labeled event, performs its work, and applies the next label to trigger the downstream stage. The pipeline state is visible on the PR at all times — no shared memory, no direct calls between workflows.
How it works
- Each pipeline stage maps to one workflow file; the trigger is a specific label value.
- A workflow reads the current label set to determine its context (e.g. attempt count), does its work, then applies the next state label.
- Re-pulse: re-applying a label that already exists on the PR emits a fresh
labeledevent, triggering the same workflow again — this is the retry mechanism without any polling or sleep. - Attempt counters are encoded directly in label names (
auto-fix-attempt-1,auto-fix-attempt-2) so the label set is the only state store needed.
Where it came from
- Formal review events were not reliable. Some repositories block Actions from submitting
REQUEST_CHANGES, so auto-fix waiting onpull_request_reviewnever fired. And re-applying a label that is already present emits no new event, so later review iterations silently skipped auto-fix. Hence labels as state, plus the remove-then-add re-pulse. → ADR-0006 - Labels are read without a lock. Two
changes-requestedevents close together started two auto-fix runs that read the sameauto-fix-attempt-Ncount. Per-PR concurrency groups now serialize the workflows. → ADR-0020
When to use
- Multi-stage pipelines running across GitHub Actions workflows where passing state via artifacts is cumbersome.
- Systems that need a human-readable audit trail of pipeline state without a separate dashboard.
- Retry loops where the re-trigger mechanism must be observable and manually overridable (operators can apply labels by hand).
When not to use
- Pipelines that need sub-second latency — GitHub Actions
labeledevent dispatch adds seconds. - More than ~6 states: label namespace becomes hard to manage and
labeledevent payloads carry no structured data beyond the label name. - Environments where GitHub is not the host — label semantics are GitHub-specific.
Trade-offs
| Pro | Pipeline state is always visible on the PR — no external dashboard needed |
| Pro | Manual override is trivial — operators apply or remove labels by hand |
| Pro | Re-pulse retry costs nothing: re-applying a label emits a fresh event |
| Con | Label event dispatch latency makes this unsuitable for tight loops |
| Con | State is stringly typed — a typo in a label name silently breaks the transition |
| Con | Attempt counters encoded in label names produce label clutter on long-running PRs |
Failure modes
- Missing label: workflow applies wrong label name (typo); downstream stage never triggers.
- Concurrent triggers: two events fire simultaneously (e.g. push + label); workflows race and produce duplicate commits.
- Escalation bypass: operator manually applies
review-approvedbefore auto-fix exhausts retries, skipping the loop guard.
Implementation reference
autonomous-dev-loop:
.github/workflows/validate-issue.yml— stage 1, emitsready-for-dev.github/workflows/code-generation.yml— stage 2, triggered byready-for-dev.github/workflows/pr-review.yml— stage 3, emitschanges-requestedorreview-approved.github/workflows/auto-fix-pr.yml— stage 4, re-pulse viachanges-requestedscripts/auto_fix_pr.mjs— reads existingauto-fix-attempt-Nlabels, increments counterconfig/labels.yaml— label names and colors as versioned configdocs/adr/ADR-0006— re-pulse strategy rationale