# How to Implement Manual Approval Gates for Critical Workflow Operations with gh-aw

> Implement manual approval gates for critical workflow operations with gh-aw. Automatically configure GitHub environment protection rules to pause execution until human approval.

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

---

**Add `manual-approval: <environment-name>` to your workflow's front-matter `on:` section, and the gh-aw compiler automatically configures GitHub environment protection rules to pause execution until a human approves the deployment.**

The `github/gh-aw` compiler transforms markdown-based workflow definitions into executable GitHub Actions by injecting activation jobs that respect environment protection rules. Implementing manual approval gates for critical workflow operations ensures that sensitive deployments require explicit human authorization before proceeding, leveraging GitHub's native environment protection capabilities.

## Declaring Manual Approval in Workflow Front-matter

To enable a manual approval gate, declare the `manual-approval` key inside the `on:` section of your workflow's YAML front-matter. The value must be a string matching a protected environment name configured in your repository settings.

```yaml
---
description: Deploy to production only after human approval
on:
  workflow_dispatch:
  manual-approval: production   # <-- protected environment name

permissions:
  contents: read
engine: copilot
---

# Your workflow steps follow here

```

The `manual-approval` key can coexist with any trigger type, such as `workflow_dispatch`, `issues`, or `schedule`. When the compiler processes this definition, it extracts the environment name and propagates it through the compilation pipeline.

## How the Compiler Processes Approval Gates

The implementation spans four core components that extract, store, annotate, and enforce the manual approval configuration.

### Extracting the Approval Configuration

File: [`pkg/workflow/manual_approval.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/manual_approval.go)

The `extractManualApprovalFromOn` function parses the front-matter map and retrieves the `manual-approval` value when the `on` section is structured as a map.

```go
func (c *Compiler) extractManualApprovalFromOn(frontmatter map[string]any) (string, error) {
    onSection, exists := frontmatter["on"]
    if !exists {
        return "", nil
    }

    switch on := onSection.(type) {
    case map[string]any:
        if manualApproval, exists := on["manual-approval"]; exists {
            if str, ok := manualApproval.(string); ok {
                return str, nil
            }
            return "", fmt.Errorf("manual-approval value must be a string, got %T", manualApproval)
        }
    }
    return "", nil
}

```

This function returns the environment name as a string or an empty string if the configuration is absent. It returns an error only when the value is present but not a string, ensuring type safety.

### Storing Approval Data in Workflow Structures

File: [`pkg/workflow/manual_approval.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/manual_approval.go) and [`pkg/workflow/compiler_types.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/compiler_types.go)

The `processManualApprovalConfiguration` method invokes the extractor and assigns the result to the `WorkflowData` struct:

```go
manualApproval, err := c.extractManualApprovalFromOn(frontmatter)
if err != nil {
    return err
}
workflowData.ManualApproval = manualApproval

```

The `WorkflowData` struct definition in [`compiler_types.go`](https://github.com/github/gh-aw/blob/main/compiler_types.go) includes:

```go
type WorkflowData struct {
    // … other fields …
    ManualApproval string // environment name for manual‑approval from on: section
}

```

### Generating Lock File Annotations

File: [`pkg/workflow/compiler_yaml.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/compiler_yaml.go)

When generating the compiled lock file, the compiler injects a comment to document the approval requirement:

```go
if data.ManualApproval != "" {
    cleanManualApproval := stringutil.StripANSIEscapeCodes(data.ManualApproval)
    fmt.Fprintf(yaml, "# Manual approval required: environment '%s'\n", cleanManualApproval)

}

```

This annotation ensures that developers reviewing the compiled workflow understand that an approval gate is active.

### Configuring the Activation Job Environment

File: [`pkg/workflow/compiler_activation_jobs.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/compiler_activation_jobs.go)

The compiler constructs the activation job that triggers the main workflow. If `ManualApproval` is set, it adds the `environment` key:

```go
var environment string
if data.ManualApproval != "" {
    // Strip ANSI escape codes from manual‑approval environment name
    cleanManualApproval := stringutil.StripANSIEscapeCodes(data.ManualApproval)
    environment = fmt.Sprintf("environment: %s", cleanManualApproval)
}
// …
job := &Job{
    Name:        string(constants.ActivationJobName),
    Environment: environment,
    // …
}

```

When GitHub Actions executes this job, it evaluates the `environment:` field. If the environment is protected with a "Required reviewers" or "Require approval before job can proceed" rule, the workflow pauses until an authorized user approves it via the GitHub UI or API.

## End-to-End Workflow Execution Flow

The complete lifecycle of a manual approval gate involves five distinct stages:

1. **Declaration**: The workflow author adds `manual-approval: <environment>` to the front-matter `on:` section.
2. **Compilation**: `gh aw compile` invokes `extractManualApprovalFromOn` to parse the value, stores it in `WorkflowData.ManualApproval`, and generates the lock file with an annotation comment.
3. **Job Generation**: The compiler creates an activation job in [`compiler_activation_jobs.go`](https://github.com/github/gh-aw/blob/main/compiler_activation_jobs.go) that includes `environment: <value>`.
4. **Execution Pause**: When the workflow triggers, GitHub detects the environment protection rules and halts the activation job, displaying a "Review pending" button in the Actions UI.
5. **Continuation**: After a user with appropriate permissions approves the deployment, the activation job completes and triggers the main agent job to proceed with the critical operation.

## Practical Implementation Examples

### Basic Workflow with Production Approval

Create a file at [`.github/workflows/deploy-prod.md`](https://github.com/github/gh-aw/blob/main/.github/workflows/deploy-prod.md) with the following content:

```yaml
---
description: Production deployment with mandatory human approval
on:
  workflow_dispatch:
  manual-approval: production
permissions:
  contents: read
  deployments: write
engine: copilot
---

## Deployment Steps

1. Build container image
2. Run security scans
3. Deploy to production cluster

```

After running `gh aw compile`, the generated lock file will contain:

```yaml

# Manual approval required: environment 'production'

jobs:
  activation:
    environment: production
    # ... additional auto-generated configuration ...

```

### Programmatic Configuration Access

For custom tooling that needs to validate or extract manual approval settings:

```go
package main

import (
    "log"
    "github.com/github/gh-aw/pkg/workflow"
)

func main() {
    compiler := workflow.NewCompiler()
    
    // Parse frontmatter from markdown content
    frontmatter, err := compiler.ParseFrontmatter(mdContent)
    if err != nil {
        log.Fatalf("Failed to parse frontmatter: %v", err)
    }
    
    // Extract manual approval configuration
    approvalEnv, err := compiler.ExtractManualApprovalFromOn(frontmatter)
    if err != nil {
        log.Fatalf("Invalid manual-approval configuration: %v", err)
    }
    
    if approvalEnv != "" {
        log.Printf("Workflow requires manual approval for environment: %s", approvalEnv)
    } else {
        log.Println("No manual approval gate configured")
    }
}

```

### Validating with Unit Tests

The repository includes comprehensive tests in [`pkg/workflow/manual_approval_test.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/manual_approval_test.go) that verify extraction logic:

```go
func TestExtractManualApproval(t *testing.T) {
    tests := []struct {
        name        string
        frontmatter map[string]any
        want        string
        wantErr     bool
    }{
        {
            name: "valid manual approval in on section",
            frontmatter: map[string]any{
                "on": map[string]any{
                    "workflow_dispatch": nil,
                    "manual-approval":   "production",
                },
            },
            want:    "production",
            wantErr: false,
        },
        {
            name: "missing manual approval",
            frontmatter: map[string]any{
                "on": map[string]any{
                    "workflow_dispatch": nil,
                },
            },
            want:    "",
            wantErr: false,
        },
    }
    
    // Test execution logic here...
}

```

Run the test suite with:

```bash
make test-unit

# or specifically:

go test ./pkg/workflow/... -run TestManualApproval

```

## Key Source Files Reference

Understanding the manual approval implementation requires familiarity with these specific files in the `github/gh-aw` repository:

- **[`pkg/workflow/manual_approval.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/manual_approval.go)** – Contains `extractManualApprovalFromOn` and `processManualApprovalConfiguration` functions that parse and validate the front-matter configuration.

- **[`pkg/workflow/manual_approval_test.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/manual_approval_test.go)** – Unit tests covering valid configurations, missing values, and type validation errors.

- **[`pkg/workflow/manual_approval_integration_test.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/manual_approval_integration_test.go)** – Integration tests verifying that the activation job correctly receives the environment configuration.

- **[`pkg/workflow/compiler_types.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/compiler_types.go)** – Defines the `WorkflowData` struct with the `ManualApproval` string field.

- **[`pkg/workflow/compiler_yaml.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/compiler_yaml.go)** – Implements the lock file comment generation that documents the approval requirement.

- **[`pkg/workflow/compiler_activation_jobs.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/compiler_activation_jobs.go)** – Constructs the activation job with the `environment:` field that triggers GitHub's protection rules.

- **[`pkg/cli/workflows/test-manual-approval.md`](https://github.com/github/gh-aw/blob/main/pkg/cli/workflows/test-manual-approval.md)** – Example workflow file demonstrating the front-matter syntax used by the test suite.

## Summary

- **Declare** manual approval gates by adding `manual-approval: <environment-name>` to the `on:` section of your workflow front-matter.
- **Compile** workflows using `gh aw compile` to generate activation jobs that include the `environment:` field required by GitHub's protection rules.
- **Extract** configuration programmatically via `extractManualApprovalFromOn` in [`pkg/workflow/manual_approval.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/manual_approval.go) for custom validation tooling.
- **Validate** implementations using the comprehensive test suite in [`pkg/workflow/manual_approval_test.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/manual_approval_test.go) and integration tests.
- **Protect** critical operations by configuring the target environment in GitHub repository settings with required reviewers or deployment protection rules.

## Frequently Asked Questions

### How do I configure the required reviewers for a manual approval gate?

Configure the protection rules directly in your GitHub repository settings under **Settings > Environments**. Select the environment name specified in your `manual-approval` value (e.g., `production`), then enable **"Required reviewers"** and specify which users or teams must approve the deployment. The `gh-aw` compiler only injects the `environment:` field into the activation job; GitHub's platform handles the actual approval UI and notifications.

### What happens if I specify a manual-approval environment that does not exist?

If the environment name specified in `manual-approval` does not exist in the repository settings, GitHub Actions will attempt to create it dynamically when the workflow runs. However, dynamically created environments do not inherit protection rules, so the workflow will execute without pausing for approval. Always pre-configure the environment in repository settings with protection rules to ensure the gate functions correctly.

### Can I use manual approval gates with triggers other than workflow_dispatch?

Yes, the `manual-approval` key functions alongside any valid GitHub Actions trigger. You can combine it with `issues`, `pull_request`, `schedule`, or custom repository events. The compiler extracts the value from the `on:` section regardless of the trigger type, and the activation job will always include the `environment:` field, causing GitHub to evaluate protection rules before proceeding.

### How do I troubleshoot when the manual approval gate is not triggering?

First, verify that the compiled lock file contains the environment declaration by checking for the comment `# Manual approval required: environment '...'` in the generated YAML. If absent, ensure your front-matter syntax is correct and that `gh aw compile` completed without errors. If the comment exists but the gate still doesn't trigger, confirm that the environment name in your repository settings exactly matches the value in `manual-approval` (case-sensitive) and that protection rules are enabled for that environment. Check the workflow run logs to see if the activation job is being created with the correct `environment:` field.