Repository: github/gh-aw (2026-04-02T10:21:27Z)

Issue:
Conclusion job concurrency is static per workflow and cannot be customized
## Summary

gh-aw currently generates a static concurrency group for the compiler-generated `conclusion` job:

`gh-aw-conclusion-<workflow-id>`

This is not configurable from frontmatter, and it causes incorrect behavior in concurrent issue-driven workflows. When several runs of the same workflow happen at nearly the same time, GitHub Actions concurrency allows one running and one pending job per group, and later requests can cancel older pending `conclusion` jobs with messages like:

`Canceling since a higher priority waiting request for gh-aw-conclusion-<workflow-id> exists.`

This means the generated `conclusion` job can be dropped even though the rest of the workflow completed successfully.

## Observed behavior

In an issue-triggered gh-aw workflow, multiple runs for different issues can complete `activation`, `agent`, `detection`, and `safe_outputs`, but the generated `conclusion` job is serialized across all runs of that workflow by a static group.

Because the group is static, concurrent runs for different issues compete for the same `conclusion` slot. If enough runs overlap, older pending conclusion jobs are cancelled by GitHub Actions queue behavior.

Observed example:

https://github.com/githubnext/gh-aw-security/actions/runs/23854981154

In that run, the main workflow work completed successfully, but the `conclusion` job was cancelled with:

`Canceling since a higher priority waiting request for gh-aw-conclusion-pentest-xpia-victim-triage exists.`

## Why this is a problem

The docs describe the `conclusion` job concurrency as preventing collisions when multiple agents run the same workflow concurrently, and say queued conclusion runs complete in order rather than being discarded.

In practice, because the generated group is static per workflow, GitHub's concurrency semantics mean this is not true under higher fan-out. The static group causes unrelated runs for different issues or inputs to interfere with each other.

This is especially problematic for:

- issue-triggered workflows
- discussion-triggered workflows
- fan-out patterns where many runs of the same compiled workflow occur close together
- workflows where `conclusion` is responsible for reporting, cleanup, or final status propagation

## Current workaround

The only effective workaround I found is patching the generated lock file after compilation so the `conclusion` job group includes a per-run discriminator, for example:

```yaml
conclusion:
  concurrency:
    group: "gh-aw-conclusion-pentest-xpia-victim-triage-${{ github.event.issue.number || github.run_id }}"
    cancel-in-progress: false
```

This fixes the cancellation behavior, but it is not viable because the change is lost on recompile.

## Root cause

The compiler currently hardcodes the conclusion concurrency group from `WorkflowID` alone.

There is no frontmatter field to influence this group:

- `concurrency.job-discriminator` only applies to compiler-generated `agent` and `output` job groups
- `safe-outputs.concurrency-group` only applies to the `safe_outputs` job
- `conclusion` concurrency has no override or discriminator support

## Verified in source

Current compiler logic in `pkg/workflow/notify_comment.go` builds the group as:

```go
group := "gh-aw-conclusion-" + data.WorkflowID
```

The corresponding test in `pkg/workflow/notify_comment_test.go` asserts the same static pattern:

```go
expectedGroup: "gh-aw-conclusion-my-workflow"
```

The docs also state that conclusion job concurrency is automatic and not configurable.

## Expected solution

One of these should be implemented:

1. Preferred: apply `concurrency.job-discriminator` to the generated `conclusion` job as well.
2. Alternatively: add an explicit frontmatter override for conclusion job concurrency.
3. At minimum: change the compiler-generated default for `conclusion` to include a run-specific discriminator for triggers where multiple instances of the same workflow can run concurrently.

## Suggested behavior

A good default would be something like:

```yaml
group: "gh-aw-conclusion-<workflow-id>-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}"
```

Or, if keeping defaults stable is important, allow users to opt in via frontmatter using an override or discriminator field.

## Additional changes needed

Please update:

- compiler implementation for conclusion job concurrency generation
- schema and frontmatter docs
- unit tests covering conclusion job concurrency
- reference docs that currently state the conclusion group prevents collisions automatically and that queued runs are not discarded

## Impact

Without this fix, concurrent runs of the same gh-aw workflow can silently lose their `conclusion` job, which breaks final reporting and cleanup semantics and makes generated workflow behavior unreliable under load.


Discussion:

Patch:
diff --git a/docs/src/content/docs/reference/concurrency.md b/docs/src/content/docs/reference/concurrency.md
index f493739c556..c58ba33ba02 100644
--- a/docs/src/content/docs/reference/concurrency.md
+++ b/docs/src/content/docs/reference/concurrency.md
@@ -85,11 +85,20 @@ This prevents conclusion jobs from colliding when multiple agents run the same w
 
 This concurrency group is set automatically during compilation and requires no manual configuration.
 
+When `concurrency.job-discriminator` is set, the discriminator is also appended to the conclusion job's concurrency group, making each run's group distinct:
+
+```yaml wrap
+concurrency:
+  job-discriminator: ${{ github.event.issue.number || github.run_id }}
+```
+
+This generates a group like `gh-aw-conclusion-my-workflow-${{ github.event.issue.number || github.run_id }}`, preventing concurrent runs for different issues or inputs from competing for the same conclusion slot.
+
 ## Fan-Out Concurrency (`job-discriminator`)
 
 When multiple workflow instances are dispatched concurrently with different inputs (fan-out pattern), compiler-generated job-level concurrency groups are static across all runs — causing all but the latest dispatched run to be cancelled as they compete for the same slot.
 
-Use `concurrency.job-discriminator` to append a unique expression to compiler-generated job-level concurrency groups (`agent` and `output` jobs), making each dispatched run's group distinct:
+Use `concurrency.job-discriminator` to append a unique expression to compiler-generated job-level concurrency groups (`agent`, `output`, and `conclusion` jobs), making each dispatched run's group distinct:
 
 ```yaml wrap
 concurrency:
diff --git a/pkg/workflow/notify_comment.go b/pkg/workflow/notify_comment.go
index 623246fcef8..c9369e0da7c 100644
--- a/pkg/workflow/notify_comment.go
+++ b/pkg/workflow/notify_comment.go
@@ -473,6 +473,12 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa
 	var concurrency string
 	if data.WorkflowID != "" {
 		group := "gh-aw-conclusion-" + data.WorkflowID
+		// If the user specified a job-discriminator, append it so that concurrent
+		// runs with different inputs (fan-out pattern) do not share the same group.
+		if data.ConcurrencyJobDiscriminator != "" {
+			notifyCommentLog.Printf("Appending job discriminator to conclusion job concurrency group: %s", data.ConcurrencyJobDiscriminator)
+			group = fmt.Sprintf("%s-%s", group, data.ConcurrencyJobDiscriminator)
+		}
 		concurrency = c.indentYAMLLines(fmt.Sprintf("concurrency:\n  group: %q\n  cancel-in-progress: false", group), "    ")
 		notifyCommentLog.Printf("Configuring conclusion job concurrency group: %s", group)
 	}


Test patch:
diff --git a/pkg/workflow/notify_comment_test.go b/pkg/workflow/notify_comment_test.go
index 4dddeaa6b02..6ae1d1cec54 100644
--- a/pkg/workflow/notify_comment_test.go
+++ b/pkg/workflow/notify_comment_test.go
@@ -802,6 +802,7 @@ func TestConclusionJobConcurrencyGroup(t *testing.T) {
 	tests := []struct {
 		name              string
 		workflowID        string
+		discriminator     string
 		expectConcurrency bool
 		expectedGroup     string
 	}{
@@ -816,14 +817,29 @@ func TestConclusionJobConcurrencyGroup(t *testing.T) {
 			workflowID:        "",
 			expectConcurrency: false,
 		},
+		{
+			name:              "job-discriminator appended to conclusion concurrency group",
+			workflowID:        "my-workflow",
+			discriminator:     "${{ github.event.issue.number || github.run_id }}",
+			expectConcurrency: true,
+			expectedGroup:     "gh-aw-conclusion-my-workflow-${{ github.event.issue.number || github.run_id }}",
+		},
+		{
+			name:              "job-discriminator with run_id for universal uniqueness",
+			workflowID:        "pentest-triage",
+			discriminator:     "${{ github.run_id }}",
+			expectConcurrency: true,
+			expectedGroup:     "gh-aw-conclusion-pentest-triage-${{ github.run_id }}",
+		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			compiler := NewCompiler()
 			workflowData := &WorkflowData{
-				Name:       "Test Workflow",
-				WorkflowID: tt.workflowID,
+				Name:                        "Test Workflow",
+				WorkflowID:                  tt.workflowID,
+				ConcurrencyJobDiscriminator: tt.discriminator,
 				SafeOutputs: &SafeOutputsConfig{
 					MissingTool: &MissingToolConfig{},
 				},