# Workflow Concurrency Control in gh-aw: How It Prevents Duplicate Runs

> Learn how gh-aw prevents duplicate workflow runs using unique concurrency group keys and GitHub Actions concurrency stanza. Ensure only one instance runs at a time.

- Repository: [GitHub/gh-aw](https://github.com/github/gh-aw)
- Tags: internals
- Published: 2026-02-16

---

**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`](https://github.com/github/gh-aw/blob/main/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:

```yaml
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`](https://github.com/github/gh-aw/blob/main/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:

```yaml
---
concurrency:
  group: "my-custom-group"
  cancel-in-progress: true
---

```

When `GenerateConcurrencyConfig` detects this existing configuration at lines 12-21 of [`pkg/workflow/concurrency.go`](https://github.com/github/gh-aw/blob/main/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:

```yaml
---
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:

```yaml
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`](https://github.com/github/gh-aw/blob/main/pkg/workflow/concurrency.go) | Core logic for `GenerateConcurrencyConfig`, `buildConcurrencyGroupKeys`, and `GenerateJobConcurrencyConfig` |
| [`pkg/workflow/concurrency_validation.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/concurrency_validation.go) | Validates syntax of user-provided `concurrency.group` expressions |
| [`pkg/workflow/engine.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/engine.go) | Extracts `engine.concurrency` from front-matter for job-level application |
| [`pkg/workflow/compiler_yaml.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/compiler_yaml.go) | Inserts generated concurrency stanzas into final compiled workflow YAML |
| [`docs/src/content/docs/reference/concurrency.md`](https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/concurrency.md) | Human-readable documentation generated from source |

## Summary

- **Automatic Detection**: `GenerateConcurrencyConfig` in [`pkg/workflow/concurrency.go`](https://github.com/github/gh-aw/blob/main/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`](https://github.com/github/gh-aw/blob/main/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`](https://github.com/github/gh-aw/blob/main/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`](https://github.com/github/gh-aw/blob/main/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`](https://github.com/github/gh-aw/blob/main/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.