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: GenerateConcurrencyConfig in pkg/workflow/concurrency.go checks for existing concurrency configurations and generates group keys only when necessary.
  • Context-Aware Grouping: The buildConcurrencyGroupKeys function 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 GenerateJobConcurrencyConfig handles agent-specific isolation for generic triggers.
  • Compiler Integration: The pkg/workflow/compiler_yaml.go file 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →