The Challenge of Parallel Execution
GitHub Actions was designed with a philosophy of parallelism. By default, when a triggering event occurs—such as a push to a main branch or the opening of a pull request—GitHub executes all matching workflows and their constituent jobs concurrently. This design choice maximizes speed for independent tasks, allowing multiple tests, builds, and checks to run simultaneously. However, this default behavior presents a significant engineering challenge for projects that require strict sequential execution. In many development scenarios, one workflow must complete successfully before the next can begin. For instance, a workflow that builds and pushes a Docker image must finish before a subsequent workflow can test that specific image.
The fundamental issue stems from the architecture of GitHub Actions: workflows are generally unaware of other workflows. They do not natively share state or execution status. While steps within a single job always execute sequentially, and jobs within a single workflow can be ordered using the needs keyword, cross-workflow sequencing requires explicit orchestration. Developers migrating from continuous integration services like Travis CI to GitHub Actions often grapple with this limitation, particularly when managing complex pipelines or optimizing usage minutes in private repositories, where the free tier offers less than 2,000 minutes per month.
Native Event-Based Sequencing
The most robust native method for chaining workflows involves utilizing the workflow_run event. This event triggers a workflow when a specified workflow finishes, regardless of its outcome. This approach eliminates the need for external tokens or API calls for basic sequencing, provided the triggering workflow has a consistent name and trigger pattern.
Consider a scenario with two distinct files: first-action.yml and second-action.yml. The first workflow, named "First action", might be scheduled to run every Monday at 17:00 or triggered manually. To ensure "Second action" runs immediately after "First action" completes, the second-action.yml file must be configured with a specific trigger configuration.
yaml
name: Second action
on:
workflow_run:
workflows: ["First action"]
types: [completed]
The workflows field specifies the name of the workflow that triggers the current one. The types field restricts the trigger to specific activity types. The available activity types for workflow_run are completed, requested, and in_progress. By setting the type to completed, the second workflow will trigger every time the first workflow finishes its execution cycle.
However, triggering on completion alone is often insufficient for production pipelines. A failed build or a failed test suite in the first workflow should not trigger a dependent testing workflow. To enforce success-based sequencing, developers must add a conditional statement using the github.event.workflow_run.conclusion property.
yaml
jobs:
run-after-success:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- run: echo "Previous workflow succeeded"
This conditional ensures that "Second action" only executes if "First action" concludes with a successful status. This method is clean, relies entirely on GitHub's native event system, and does not consume additional API rate limits or require secret management.
API-Driven Daisy Chaining
When native event triggering is insufficient—for example, when workflows need to be triggered by external events or when complex logic determines the next step—the repository_dispatch event serves as a powerful alternative. This event allows external webhooks or internal GitHub Actions workflows to trigger a new workflow run. This technique is often referred to as "daisy-chaining."
The repository_dispatch event is documented as a mechanism to trigger a webhook event when activity outside of GitHub needs to trigger a GitHub Actions workflow or GitHub App webhook. Within the context of GitHub Actions, this is implemented by issuing a repository_dispatch API call at the end of a workflow.
To implement this, developers have two primary options: executing a curl command directly in a shell step or using a specialized action from the GitHub Marketplace. The latter, such as Peter Evans's "Repository Dispatch" action, simplifies the process by handling the HTTP requests and payload formatting.
A critical requirement for repository_dispatch is authentication. The triggering workflow must have the permissions to dispatch events to the repository. This requires the creation of a Personal Access Token (PAT) with the appropriate scopes. For public repositories, the public_repo scope is sufficient. For private repositories, the repo scope is required to grant full control of private repositories.
```yaml
Example of adding PAT as a secret
1. Generate PAT with 'repo' or 'public_repo' scope
2. Add PAT to repository secrets as GH_TOKEN
3. Use in workflow:
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.GH_TOKEN }}
event-type: trigger-next-workflow
```
This method offers flexibility. A single workflow can trigger multiple subsequent workflows by issuing dispatch calls with different event types. Each subsequent workflow can listen for its specific event type. However, this approach introduces complexity in secret management and potential race conditions if not carefully orchestrated. Additionally, developers must consider the cost implications. While public repositories enjoy unlimited minutes, private repositories are capped. Sequential execution can help manage this cap by preventing parallel runs of heavy workflows when upstream tasks fail, thereby preserving minutes for successful runs.
Intra-Workflow Job Dependency
Before reaching for cross-workflow solutions, it is essential to evaluate whether the requirement can be met within a single workflow file. GitHub Actions provides native mechanisms to enforce sequential execution of jobs within the same workflow.
The needs keyword allows a job to depend on the completion of another job. This creates a dependency graph that GitHub respects during execution.
```yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "Building..."
test:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Testing..."
```
In this configuration, the test job will not start until the build job has finished. This is the standard and most efficient way to sequence tasks. However, complications arise when dealing with optional jobs or jobs that may be skipped.
If a job is skipped (due to an if condition evaluating to false), a dependent job using the default needs behavior will also be skipped. This is often undesirable in complex pipelines where a final reporting or cleanup job must run regardless of the status of previous jobs.
```yaml
jobs:
optional-first-job:
if: some.condition # This job may or may not run
runs-on: ubuntu-latest
steps:
- run: echo "Optional step"
sequential-second-job:
needs: optional-first-job
if: always() # This job always runs, but waits until needed jobs are completed
runs-on: ubuntu-latest
steps:
- run: echo "Always runs after optional job"
```
The if: always() condition ensures that the second job executes regardless of whether the first job succeeded, failed, or was skipped. This pattern is crucial for logging, cleanup, or notification tasks that must occur at the end of a pipeline. Developers can further refine this logic by combining always() with checks on the needs context, such as needs.optional-first-job.result != 'failed', to create sophisticated branching logic within a single workflow file.
Strategic Considerations for Implementation
Choosing the right sequencing strategy depends on the complexity of the project, the repository visibility, and the specific dependencies between tasks.
For simple, linear pipelines, intra-workflow job dependencies using needs are the most efficient and readable solution. They avoid the overhead of separate workflow runs and are easy to debug.
For scenarios where workflows must be completely decoupled—for example, when different teams manage different parts of the build process, or when workflows reside in different repositories—the workflow_run event is the preferred native solution. It requires no external tokens and leverages GitHub's event bus effectively.
When external triggers or complex conditional logic based on non-GitHub events are required, repository_dispatch provides the necessary flexibility. However, it requires careful management of Personal Access Tokens and an understanding of scope permissions.
Cost optimization is another critical factor. In private repositories, where minutes are limited, preventing the execution of downstream workflows when upstream workflows fail is essential. Both the workflow_run approach with success conditions and the repository_dispatch approach with conditional triggers can help conserve minutes by ensuring that only successful paths proceed. Conversely, using if: always() in intra-workflow dependencies ensures that necessary cleanup or reporting occurs even in failure scenarios, providing valuable debugging information without wasting resources on subsequent build steps.
Conclusion
Sequential execution in GitHub Actions is not a default feature but a capability that must be explicitly designed. The platform’s default parallelism is powerful for independent tasks but requires specific strategies for dependent workflows. Developers have three primary tools at their disposal: intra-workflow job dependencies for tasks within a single file, the workflow_run event for native cross-workflow chaining, and repository_dispatch for external or API-driven triggering.
Each method has distinct advantages and trade-offs. Intra-workflow dependencies offer the simplest syntax and best performance for contained pipelines. The workflow_run event provides a clean, token-free solution for sequential workflows that respond to the success or failure of other workflows. Repository_dispatch offers the highest flexibility for complex, multi-repository, or externally triggered pipelines but requires careful secret management.
Understanding these mechanisms allows engineers to build robust, efficient, and cost-effective CI/CD pipelines. By leveraging the correct sequencing strategy, teams can ensure that their workflows execute in the correct order, handle failures gracefully, and optimize their usage of GitHub Actions resources.