Github Actions General Created: 10 Apr 2026 Updated: 10 Apr 2026

Managing Workflow Execution

GitHub Actions workflows are declarative — you describe what should happen and when. So far we have used simple triggers such as push and pull_request. In this lesson we explore how to refine those triggers with activity types, branch/tag/path filters, manual and programmatic dispatch events, cross-workflow chaining, and concurrency control.

Activity Types

Many events in GitHub carry sub-events called activity types. By default a workflow that listens to pull_request fires on the opened, synchronize, and reopened types. You can override those defaults with the types keyword.

on:
pull_request:
types: [opened, ready_for_review]
issues:
types: [opened, labeled]

The example above restricts the workflow to run only when a pull request is first opened or marked ready for review, and when an issue is opened or a label is added. Without the types filter every default activity type for that event would trigger a run.

Common Events and Their Activity Types

EventDefault TypesOther Useful Types
pull_requestopened, synchronize, reopenedclosed, ready_for_review, labeled, review_requested
issuesopened, edited, deleted, transferred, pinned, unpinned, closed, reopened, assigned, unassigned, labeled, unlabeled, etc.milestoned, demilestoned, locked, unlocked
labelcreated, edited, deleted
discussioncreated, edited, deleted, transferred, etc.answered, unanswered, labeled, unlabeled
Tip: Always restrict activity types to only what your workflow actually needs. Running on every default type wastes runner minutes and can produce confusing status checks.

Filtering by Branch, Tag, and Path

For push-based and pull-request-based events you can narrow execution to specific branches, tags, or file paths. GitHub provides two complementary approaches: include filters and ignore filters.

Branch and Tag Filters

The branches keyword accepts a list of branch names or glob patterns. When present, the workflow only runs if the push or PR targets a matching branch.

on:
push:
branches:
- main
- 'release/**'
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'

This workflow triggers on pushes to main, any branch under release/, or any tag matching a semantic-version pattern like v2.1.0.

Ignore Filters

Instead of listing branches to include you can list branches to exclude. You cannot mix branches and branches-ignore on the same event.

on:
push:
branches-ignore:
- 'dependabot/**'
- 'experiment/**'

Path Filters

Path filters restrict a workflow to run only when files matching a pattern change. This is especially useful in monorepos where a single push may touch many services.

on:
push:
branches: [main]
paths:
- 'backend/**'
- 'shared-lib/**'

The workflow above ignores changes to the frontend, documentation, or any other folder. A complementary paths-ignore filter is also available:

on:
push:
paths-ignore:
- 'docs/**'
- '**.md'

Glob Patterns

GitHub Actions supports a rich set of glob characters inside filter values:

PatternDescriptionExample Match
*Matches any character except /feature-* matches feature-login
**Matches any number of path segmentssrc/** matches src/a/b/c.ts
?Matches exactly one characterrelease-?.0 matches release-3.0
+Matches one or more of the preceding characterv1.0.0+ matches v1.0.00
[...]Matches a character range or setv[0-9] matches v3
!Negates a previously matched patternSee below

Negation with !

The ! character must come after a positive match to carve out an exception. Order matters: GitHub processes filter lines top-to-bottom.

on:
push:
branches:
- 'release/**'
- '!release/**-alpha'

This triggers on all release/ branches except those ending in -alpha. If the ! line came first it would have no effect because there would be nothing to negate yet.

Important: You cannot combine branches and branches-ignore for the same event. Use branches with negation patterns instead. The same rule applies to tags/tags-ignore and paths/paths-ignore.

Triggering Without a Code Change

Not every workflow should be tied to a push or pull request. GitHub provides several events that let you run workflows manually, from external systems, or as a reaction to another workflow.

workflow_dispatch — Manual Trigger

Adding workflow_dispatch to the on block exposes a Run workflow button in the GitHub Actions UI. You can optionally define typed inputs that the user fills in before clicking the button.

on:
workflow_dispatch:
inputs:
log_level:
description: 'Logging verbosity'
required: true
default: 'info'
type: choice
options:
- debug
- info
- warn
- error
dry_run:
description: 'Simulate without deploying'
required: false
type: boolean
default: false

jobs:
run-task:
runs-on: ubuntu-latest
steps:
- run: echo "Level=${{ inputs.log_level }} DryRun=${{ inputs.dry_run }}"

Supported input types include string, boolean, choice, and environment. Manual runs can also be started from the GitHub CLI (gh workflow run) or the REST API.

repository_dispatch — External Trigger

If you need an external system (a Slack bot, a monitoring tool, or another service) to kick off a workflow, repository_dispatch lets you fire a custom event through the GitHub REST API.

on:
repository_dispatch:
types: [deploy-request, rollback-request]

jobs:
handle-event:
runs-on: ubuntu-latest
steps:
- run: |
echo "Event type: ${{ github.event.action }}"
echo "Requester: ${{ github.event.client_payload.requester }}"

To fire the event from curl:

curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $TOKEN" \
https://api.github.com/repos/OWNER/REPO/dispatches \
-d '{"event_type":"deploy-request","client_payload":{"requester":"ops-bot"}}'
Note: Every workflow in the repository that listens for the same event_type will run. Use the types filter to limit which workflows respond.

workflow_call — Reusable Workflow Trigger

A workflow with workflow_call acts as a reusable building block. Other workflows invoke it with the uses keyword at the job level. The called workflow can accept inputs, receive secrets, and return outputs.

# .github/workflows/lint-and-test.yml (reusable)
on:
workflow_call:
inputs:
node_version:
required: true
type: string
secrets:
NPM_TOKEN:
required: false

jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}
- run: npm ci
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: npm run lint && npm test

A caller workflow invokes it like this:

# .github/workflows/ci.yml
on: [push]

jobs:
lint-and-test:
uses: ./.github/workflows/lint-and-test.yml
with:
node_version: '20'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

workflow_run — Chaining Workflows

Use workflow_run to start a workflow after another workflow finishes. This is useful for separating concerns: a CI workflow runs tests, then a separate deployment workflow executes only if CI succeeded.

on:
workflow_run:
workflows: ["CI Pipeline"]
types: [completed]
branches: [main]

jobs:
deploy:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Deploying because CI passed on main"

workflows

A list of workflow names (the name: field, not the file name) to watch.

types

Either completed (finished, regardless of result) or requested (queued to run).

branches

Optionally restrict to runs triggered on specific branches.

Warning: workflow_run always fires on the default branch of the repository. Your workflow file must exist on the default branch for it to work. The if condition checking conclusion == 'success' is essential — otherwise the downstream workflow runs even when the upstream failed.

Concurrency

When you push several commits in quick succession, multiple runs of the same workflow can queue up and waste runner minutes. The concurrency key lets you group runs and optionally cancel redundant ones.

concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true

Every run that shares the same group value is treated as part of a single concurrency lane. If a new run enters the group while an older run is still in progress, cancel-in-progress: true cancels the older run automatically.

Choosing a Group Name

The group string is an expression, so you can build dynamic names:

PatternEffect
ci-${{ github.ref }}One lane per branch — pushes to the same branch cancel each other
deploy-productionA single global lane — only one production deploy at a time
pr-${{ github.event.pull_request.number }}One lane per pull request

Workflow-Level vs. Job-Level Concurrency

You can place the concurrency block at the top level (applies to the entire workflow) or inside an individual job. When placed on a job, other jobs in the same workflow are not affected.

jobs:
deploy:
runs-on: ubuntu-latest
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false
steps:
- run: echo "Only one deploy per branch at a time"
Tip: Set cancel-in-progress: false for deployments. Canceling a half-finished deploy can leave your environment in a broken state. Use cancel-in-progress: true for CI checks where only the latest result matters.

Key Takeaways

  1. Use types to restrict events to the exact activity types your workflow needs.
  2. Branch, tag, and path filters let you run workflows only when relevant code changes.
  3. Glob patterns (*, **, ?, !) give fine-grained control inside filters.
  4. workflow_dispatch adds a manual trigger with typed inputs.
  5. repository_dispatch lets external systems fire custom events via the REST API.
  6. workflow_call turns a workflow into a reusable building block.
  7. workflow_run chains workflows so one starts after another completes.
  8. concurrency groups prevent redundant runs and wasted runner minutes.

Complete Working Example

The workflow below demonstrates activity types, branch and path filters, manual dispatch with inputs, and concurrency control in a single file.

# ---------------------------------------------------------------
# Workflow: Managing Workflow Execution
# Purpose : Demonstrate activity types, filters, dispatch inputs,
# and concurrency in one educational workflow.
# ---------------------------------------------------------------
name: Managed Execution Demo

# ---------------------------------------------------------------
# TRIGGERS
# We combine multiple events to showcase different trigger styles.
# ---------------------------------------------------------------
on:
# 1) Push trigger with branch and path filters.
# Only fires on pushes to main or release/* branches
# that touch files inside the src/ or config/ directories.
push:
branches:
- main
- 'release/**'
paths:
- 'src/**'
- 'config/**'

# 2) Pull-request trigger with activity-type restrictions.
# We only care about newly opened PRs and those marked
# ready for review — not every synchronize push.
pull_request:
types: [opened, ready_for_review]
branches: [main]

# 3) Manual trigger so the team can run this on demand
# directly from the Actions tab or via "gh workflow run".
workflow_dispatch:
inputs:
environment:
description: 'Target environment for the deploy step'
required: true
default: 'staging'
type: choice
options:
- staging
- production
skip_tests:
description: 'Skip the test job (use with caution)'
required: false
type: boolean
default: false

# ---------------------------------------------------------------
# CONCURRENCY
# Group runs by branch so rapid pushes to the same branch cancel
# the previous in-progress run instead of queuing up.
# ---------------------------------------------------------------
concurrency:
group: managed-exec-${{ github.ref }}
cancel-in-progress: true

# ---------------------------------------------------------------
# JOBS
# ---------------------------------------------------------------
jobs:
# ── Job 1: Lint ──────────────────────────────────────────────
# Runs a quick syntax check on every trigger.
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# A lightweight lint step — any linter could go here.
- name: Run linter
run: echo "Linting source files in src/ ..."

# ── Job 2: Test ──────────────────────────────────────────────
# Runs the test suite unless the caller explicitly skips it
# via the workflow_dispatch "skip_tests" input.
test:
needs: lint
if: ${{ github.event.inputs.skip_tests != 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Run tests
run: echo "Running unit and integration tests ..."

# ── Job 3: Deploy ───────────────────────────────────────────
# Deploys to the environment chosen in the manual trigger.
# Falls back to "staging" when the workflow is started by push
# or pull_request (where the input is not available).
deploy:
needs: [lint, test]
if: always() && needs.lint.result == 'success' && (needs.test.result == 'success' || needs.test.result == 'skipped')
runs-on: ubuntu-latest

# Job-level concurrency prevents overlapping deploys to the
# same environment even if workflow-level concurrency allows
# multiple branches to run in parallel.
concurrency:
group: deploy-${{ github.event.inputs.environment || 'staging' }}
cancel-in-progress: false

steps:
- uses: actions/checkout@v4

- name: Deploy
run: |
ENV="${{ github.event.inputs.environment || 'staging' }}"
echo "Deploying to $ENV ..."
Share this lesson: