Workflow Concurrency Control in gh-aw: How It Prevents Duplicate Runs
gh-aw guarantees only one instance of a workflow runs at a time by generating unique concurrency group keys and injecting them into the GitHub Actions concurrency stanza.
Workflow concurrency control is essential for preventing redundant CI/CD executions when multiple events trigger the same pipeline. In the github/gh-aw repository, this mechanism is implemented through an intelligent grouping system that automatically generates unique identifiers based on trigger context. By analyzing workflow definitions and event types, gh-aw constructs concurrency configurations that ensure only one run proceeds per logical group while optionally canceling stale in-progress jobs.
How Workflow Concurrency Control Works in gh-aw
Detecting When to Apply Concurrency
The process begins in pkg/workflow/concurrency.go with the GenerateConcurrencyConfig function. This function checks whether the workflow author has already defined a concurrency block in the front-matter. If present, the configuration is returned unchanged according to the logic at lines 12-21.
When no explicit configuration exists, gh-aw invokes buildConcurrencyGroupKeys (lines 28-59) to construct a unique identifier. This function examines the trigger type—whether it is a pull request, issue, discussion, or branch push—to generate a deterministic group key that prevents overlapping executions for the same logical context.
Creating the YAML Concurrency Block
The generated group key is interpolated into a standard GitHub Actions concurrency stanza. For pull request workflows, the shouldEnableCancelInProgress function (lines 31-35) automatically appends cancel-in-progress: true, ensuring that new commits to a PR terminate any previous in-progress runs for that same PR.
The resulting YAML structure follows this pattern:
concurrency:
group: "gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
cancel-in-progress: true
Job-Level Concurrency for the Agent
Beyond workflow-level controls, gh-aw implements job-specific concurrency through GenerateJobConcurrencyConfig. This function, also located in pkg/workflow/concurrency.go, handles the agent job specifically.
If the engine's front-matter defines engine.concurrency, that configuration is used verbatim (lines 44-49). For generic triggers like workflow_dispatch, the system falls back to a default pattern gh-aw-<engine-id>-{{ github.workflow }} (lines 58-74), preventing multiple agents from competing for the same workflow run.
Special trigger types—including issues, pull requests, discussions, push events, and command workflows—skip job-level concurrency because they are already managed at the workflow level (lines 51-56).
Concurrency Group Key Patterns by Trigger Type
The specific group key patterns vary by event type to ensure appropriate isolation:
| Trigger Type | Group Key Pattern | Cancel-In-Progress |
|---|---|---|
| Pull Request | ${{ github.event.pull_request.number || github.ref }} |
true (cancels older PR runs) |
| Issue | ${{ github.event.issue.number }} |
false (separate runs per issue) |
| Push | ${{ github.ref }} (branch reference) |
false (parallel runs on different branches) |
| Command | ${{ github.event.issue.number || github.event.pull_request.number }} |
false (single execution per issue/PR) |
These patterns ensure that a push to the main branch does not interfere with a pull request workflow, while multiple commits to the same PR automatically cancel previous in-progress runs to conserve resources.
Practical Code Examples
Overriding with Explicit Front-Matter
Workflow authors can bypass automatic generation by defining their own concurrency block:
---
concurrency:
group: "my-custom-group"
cancel-in-progress: true
---
When GenerateConcurrencyConfig detects this existing configuration at lines 12-21 of pkg/workflow/concurrency.go, it returns the user-defined values unchanged.
Agent-Level Configuration
For engines requiring specific agent isolation, define engine.concurrency in the front-matter:
---
engine:
id: "myengine"
concurrency:
group: "agent-{{ github.workflow }}"
cancel-in-progress: true
---
GenerateJobConcurrencyConfig extracts this configuration at lines 44-49 and injects it directly into the agent job definition.
Default Behavior for Workflow Dispatch
When triggering via workflow_dispatch without explicit configuration, gh-aw automatically generates:
concurrency:
group: "gh-aw-myengine-${{ github.workflow }}"
This default pattern, established at lines 58-74, ensures that multiple manual triggers of the same engine do not execute simultaneously.
Key Implementation Files
| File | Purpose |
|---|---|
pkg/workflow/concurrency.go |
Core logic for GenerateConcurrencyConfig, buildConcurrencyGroupKeys, and GenerateJobConcurrencyConfig |
pkg/workflow/concurrency_validation.go |
Validates syntax of user-provided concurrency.group expressions |
pkg/workflow/engine.go |
Extracts engine.concurrency from front-matter for job-level application |
pkg/workflow/compiler_yaml.go |
Inserts generated concurrency stanzas into final compiled workflow YAML |
docs/src/content/docs/reference/concurrency.md |
Human-readable documentation generated from source |
Summary
- Automatic Detection:
GenerateConcurrencyConfiginpkg/workflow/concurrency.gochecks for existing concurrency configurations and generates group keys only when necessary. - Context-Aware Grouping: The
buildConcurrencyGroupKeysfunction creates unique identifiers based on trigger types (PRs, issues, branches) to isolate workflow runs appropriately. - Cancel-In-Progress: Pull request workflows automatically receive
cancel-in-progress: true, terminating stale runs when new commits arrive. - Dual-Level Control: Workflow-level concurrency prevents duplicate runs across the entire workflow, while
GenerateJobConcurrencyConfighandles agent-specific isolation for generic triggers. - Compiler Integration: The
pkg/workflow/compiler_yaml.gofile ensures generated concurrency stanzas are properly inserted into the final GitHub Actions YAML output.
Frequently Asked Questions
How does gh-aw handle workflows that already define their own concurrency block?
When a workflow includes an explicit concurrency definition in its front-matter, GenerateConcurrencyConfig detects this configuration at lines 12-21 of pkg/workflow/concurrency.go and returns it unchanged. This allows workflow authors to override automatic group key generation with custom logic or specific grouping requirements.
Why does gh-aw enable cancel-in-progress only for pull request workflows?
The shouldEnableCancelInProgress function specifically checks for pull request triggers to optimize developer feedback loops. When new commits are pushed to a PR, continuing to run outdated workflow instances wastes compute resources and provides stale status checks. By setting cancel-in-progress: true for PRs, gh-aw ensures only the latest commit's workflow runs, while other trigger types like issues or pushes retain parallel execution capabilities.
What prevents multiple agent jobs from running simultaneously on the same workflow?
For generic triggers such as workflow_dispatch, GenerateJobConcurrencyConfig generates a default group key pattern gh-aw-<engine-id>-{{ github.workflow }} at lines 58-74 of pkg/workflow/concurrency.go. This creates a separate concurrency scope for the agent job itself, ensuring that even if the workflow-level concurrency allows parallel runs, the agent job serializes execution per engine instance. Special triggers like PRs or issues skip this job-level control because they are already managed at the workflow level.
How does gh-aw validate custom concurrency group expressions?
User-provided concurrency.group expressions undergo syntax validation through pkg/workflow/concurrency_validation.go. This file checks that custom group keys contain valid GitHub Actions expression syntax and reference allowed context objects, preventing compilation errors that would otherwise surface only at runtime when GitHub Actions attempts to parse the generated workflow YAML.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →