{"id":76,"date":"2025-12-24T12:00:00","date_gmt":"2025-12-24T12:00:00","guid":{"rendered":"https:\/\/rezachegini.com\/?p=76"},"modified":"2026-10-30T06:48:11","modified_gmt":"2026-10-30T06:48:11","slug":"reusable-workflows-concurrency-in-github-actions","status":"publish","type":"post","link":"https:\/\/rezachegini.com\/?p=76","title":{"rendered":"Reusable Workflows &#038; Concurrency in GitHub Actions"},"content":{"rendered":"<p class=\"wp-block-paragraph\">After building custom actions, I discovered an even more powerful concept: reusable workflows. While custom actions let you reuse steps, reusable workflows let you reuse entire jobs and workflows. I&#8217;ll also show you how to manage concurrent workflow runs.<\/p>\n<h3 class=\"wp-block-heading\">Reusable Workflows vs Custom Actions<\/h3>\n<p class=\"wp-block-paragraph\">Before diving in, let me clarify the difference because I was confused at first:<\/p>\n<p class=\"wp-block-paragraph\"><strong>Custom Actions<\/strong> (from my last article):<\/p>\n<ul class=\"wp-block-list\"><li>Reuse steps within a job<\/li><li>Run on the same runner as the calling job<\/li><li>Can be composite, JavaScript, or Docker<\/li><li>Good for small, reusable tasks<\/li><\/ul>\n<p class=\"wp-block-paragraph\"><strong>Reusable Workflows<\/strong>:<\/p>\n<ul class=\"wp-block-list\"><li>Reuse entire workflows with multiple jobs<\/li><li>Each job runs on its own runner<\/li><li>Always YAML-based<\/li><li>Good for complete CI\/CD pipelines<\/li><\/ul>\n<h3 class=\"wp-block-heading\">Creating a Reusable Workflow<\/h3>\n<p class=\"wp-block-paragraph\">Here&#8217;s my reusable deployment workflow that I can call from other workflows:<\/p>\n<p class=\"wp-block-paragraph\">File: workflows\/18-1-reusable-workflows.yaml<\/p>\n<pre class=\"wp-block-code\"><code>name: 18 - 1 - Reusable Workflows - Reusable Definition\n\non:\n  workflow_call:\n    inputs:\n      target-directory:\n        type: string\n        required: true\n    outputs:\n      build-status:\n        description: The status of the build process\n        value: ${{ jobs.deploy.outputs.build-status }}\n      url:\n        description: The url of the deployed version\n        value: ${{ jobs.deploy.outputs.url }}\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    outputs:\n      build-status: ${{ steps.build.outputs.build-status }}\n      url: ${{ steps.deploy.outputs.url }}\n    steps:\n      - name: Checkout repo\n        uses: actions\/checkout@v4\n      \n      - name: Build\n        id: build\n        run: |\n          echo \"Building using directory ${{ inputs.target-directory }}\"\n          echo \"build-status=success\" &gt;&gt; \"$GITHUB_OUTPUT\"\n      \n      - name: Deploy\n        id: deploy\n        run: |\n          echo \"Deploying build artifacts\"\n          echo \"url=https:\/\/www.google.com\" &gt;&gt; \"$GITHUB_OUTPUT\"<\/code><\/pre>\n<h3 class=\"wp-block-heading\">Understanding the Key Differences<\/h3>\n<p class=\"wp-block-paragraph\">1. The trigger is workflow_call:<\/p>\n<pre class=\"wp-block-code\"><code>on:\n  workflow_call:<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This is the key! It tells GitHub this workflow can be called by other workflows, not triggered directly.<\/p>\n<p class=\"wp-block-paragraph\">2. Inputs are defined at workflow level:<\/p>\n<pre class=\"wp-block-code\"><code>on:\n  workflow_call:\n    inputs:\n      target-directory:\n        type: string\n        required: true<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Unlike workflow_dispatch inputs, these are meant for other workflows to pass data.<\/p>\n<p class=\"wp-block-paragraph\">3. Outputs are defined at workflow level:<\/p>\n<pre class=\"wp-block-code\"><code>on:\n  workflow_call:\n    outputs:\n      build-status:\n        description: The status of the build process\n        value: ${{ jobs.deploy.outputs.build-status }}\n      url:\n        description: The url of the deployed version\n        value: ${{ jobs.deploy.outputs.url }}<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Notice the syntax: value: ${{ jobs.JOB_NAME.outputs.OUTPUT_NAME }}<\/p>\n<p class=\"wp-block-paragraph\">The workflow exposes outputs from its jobs so callers can use them.<\/p>\n<h3 class=\"wp-block-heading\">Calling a Reusable Workflow &#8211; Same Repository<\/h3>\n<p class=\"wp-block-paragraph\">Here&#8217;s how I call my reusable workflow from another workflow in the same repo:<\/p>\n<p class=\"wp-block-paragraph\">File: workflows\/18-2-reusable-workflows.yaml<\/p>\n<pre class=\"wp-block-code\"><code>name: 18 - 2 - Reusable Workflows\n\non: workflow_dispatch\n\njobs:\n  deploy:\n    uses: .\/.github\/workflows\/18-1-reusable-workflows.yaml\n    with:\n      target-directory: dummmy-dir\n  \n  print-outputs:\n    runs-on: ubuntu-latest\n    needs: deploy\n    steps:\n      - name: Print outputs\n        run: |\n          echo \"Build status: ${{ needs.deploy.outputs.build-status }}\"\n          echo \"URL: ${{ needs.deploy.outputs.url }}\"<\/code><\/pre>\n<h3 class=\"wp-block-heading\">How It Works<\/h3>\n<p class=\"wp-block-paragraph\">1. Call the workflow with uses:<\/p>\n<pre class=\"wp-block-code\"><code>deploy:\n  uses: .\/.github\/workflows\/18-1-reusable-workflows.yaml<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Path format: .\/.github\/workflows\/FILENAME.yaml<\/p>\n<p class=\"wp-block-paragraph\">2. Pass inputs with with:<\/p>\n<pre class=\"wp-block-code\"><code>with:\n  target-directory: dummmy-dir<\/code><\/pre>\n<p class=\"wp-block-paragraph\">3. Access outputs with needs.JOB_NAME.outputs<\/p>\n<pre class=\"wp-block-code\"><code>needs: deploy\nsteps:\n  - name: Print outputs\n    run: |\n      echo \"Build status: ${{ needs.deploy.outputs.build-status }}\"\n      echo \"URL: ${{ needs.deploy.outputs.url }}\"<\/code><\/pre>\n<p class=\"wp-block-paragraph\">The deploy job isn&#8217;t a regular job &#8211; it&#8217;s calling another workflow. But you can still use needs: and get its outputs!<\/p>\n<h3 class=\"wp-block-heading\">Calling a Reusable Workflow &#8211; Different Repository<\/h3>\n<p class=\"wp-block-paragraph\">This is where it gets really powerful. You can call workflows from other repositories!<\/p>\n<p class=\"wp-block-paragraph\">File: workflows\/18-3-reusable-workflows.yaml<\/p>\n<pre class=\"wp-block-code\"><code>name: 18 - 3 - Reusable Workflows\n\non: workflow_dispatch\n\njobs:\n  deploy:\n    uses: .\/.github\/workflows\/18-1-reusable-workflows.yaml\n    with:\n      target-directory: dummmy-dir\n  \n  e2e-tests:\n    uses: udemy-lauromueller\/github-actions-course-example-e2e\/.github\/workflows\/e2e.yaml@main\n    needs: deploy\n    secrets:\n      access-token: ${{ secrets.GH_TOKEN }}<\/code><\/pre>\n<h3 class=\"wp-block-heading\">Cross-Repository Workflow Calls<\/h3>\n<p class=\"wp-block-paragraph\">The syntax:<\/p>\n<pre class=\"wp-block-code\"><code>uses: OWNER\/REPO\/.github\/workflows\/FILENAME.yaml@REF<\/code><\/pre>\n<ul class=\"wp-block-list\"><li>OWNER\/REPO &#8211; GitHub username\/organization and repository name<\/li><li>.github\/workflows\/FILENAME.yaml &#8211; Path to the workflow file<\/li><li>@REF &#8211; Branch, tag, or commit SHA (I use @main)<\/li><\/ul>\n<p class=\"wp-block-paragraph\">Example:<\/p>\n<pre class=\"wp-block-code\"><code>uses: udemy-lauromueller\/github-actions-course-example-e2e\/.github\/workflows\/e2e.yaml@main<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This calls the e2e.yaml workflow from another repository&#8217;s main branch.<\/p>\n<h3 class=\"wp-block-heading\">Passing Secrets to External Workflows<\/h3>\n<pre class=\"wp-block-code\"><code>secrets:\n  access-token: ${{ secrets.GH_TOKEN }}<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Reusable workflows don&#8217;t automatically get access to secrets. You must explicitly pass them.<\/p>\n<p class=\"wp-block-paragraph\">Here&#8217;s the reusable workflow that accepts secrets:<\/p>\n<p class=\"wp-block-paragraph\">File: workflows\/e2e.yaml<\/p>\n<pre class=\"wp-block-code\"><code>name: E2E tests\n\non:\n  workflow_dispatch:\n  workflow_call:\n    secrets:\n      access-token:\n        required: true\n\njobs:\n  e2e-tests:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Print github owner and repo\n        run: |\n          echo \"Github owner and repo: ${{ github.repository }}\"\n      \n      - name: Checkout code\n        uses: actions\/checkout@v4\n        with:\n          repository: udemy-lauromueller\/github-actions-course-example-e2e\n          ref: main\n          token: ${{ secrets.access-token || secrets.GITHUB_TOKEN }}\n      \n      - name: Setup node\n        uses: actions\/setup-node@v4\n        with:\n          node-version: '20.x'\n      \n      - name: Install dependencies\n        run: npm ci\n      \n      - name: Run E2E tests\n        run: npm run test:e2e<\/code><\/pre>\n<h3 class=\"wp-block-heading\">Understanding the Secret Declaration<\/h3>\n<pre class=\"wp-block-code\"><code>on:\n  workflow_call:\n    secrets:\n      access-token:\n        required: true<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This workflow accepts a secret named access-token. The calling workflow must provide it.<\/p>\n<p class=\"wp-block-paragraph\">Using the secret:<\/p>\n<pre class=\"wp-block-code\"><code>token: ${{ secrets.access-token || secrets.GITHUB_TOKEN }}<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This tries to use the passed access-token, and falls back to GITHUB_TOKEN if not provided.<\/p>\n<h3 class=\"wp-block-heading\">Reusable Workflows: What I Learned<\/h3>\n<h3 class=\"wp-block-heading\">1. You Can Have Multiple Triggers<\/h3>\n<pre class=\"wp-block-code\"><code>on:\n  workflow_dispatch:\n  workflow_call:\n    inputs:\n      target-directory:\n        type: string\n        required: true<\/code><\/pre>\n<p class=\"wp-block-paragraph\">My e2e workflow can be called by other workflows OR triggered manually. This is useful for testing.<\/p>\n<h3 class=\"wp-block-heading\">2. Reusable Workflows Have Limits<\/h3>\n<ul class=\"wp-block-list\"><li>Maximum 4 levels of nesting (workflow calls workflow calls workflow&#8230;)<\/li><li>Cannot call workflows from private repos in different organizations<\/li><li>The calling workflow and reusable workflow share the same GITHUB_TOKEN permissions<\/li><\/ul>\n<h3 class=\"wp-block-heading\">3. Context Variables Behave Differently<\/h3>\n<p class=\"wp-block-paragraph\">When you call a reusable workflow:<\/p>\n<ul class=\"wp-block-list\"><li>github.repository in the reusable workflow shows the CALLING repository, not the reusable workflow&#8217;s repository<\/li><li>github.ref shows the CALLING workflow&#8217;s branch<\/li><li>github.sha shows the CALLING workflow&#8217;s commit<\/li><\/ul>\n<p class=\"wp-block-paragraph\">This was confusing at first! The reusable workflow runs in the context of the caller.<\/p>\n<h3 class=\"wp-block-heading\">4. You Can&#8217;t Use needs Between Jobs in Different Workflows<\/h3>\n<p class=\"wp-block-paragraph\">This doesn&#8217;t work:<\/p>\n<pre class=\"wp-block-code\"><code># In the calling workflow, you can't do this:\n# (This would be trying to access a job inside the reusable workflow)\nneeds: job1  # This works within the reusable workflow\n\n# But from the caller, you can only use:\nneeds: deploy  # The entire reusable workflow call<\/code><\/pre>\n<p class=\"wp-block-paragraph\">But you can&#8217;t have a job in the calling workflow that needs a job inside the reusable workflow. You can only use needs with the entire reusable workflow call.<\/p>\n<h3 class=\"wp-block-heading\">Concurrency: Preventing Parallel Runs<\/h3>\n<p class=\"wp-block-paragraph\">Now let&#8217;s talk about concurrency. Sometimes you don&#8217;t want multiple workflow runs happening at the same time.<\/p>\n<h3 class=\"wp-block-heading\">Workflow-Level Concurrency<\/h3>\n<p class=\"wp-block-paragraph\">File: workflows\/19-1-concurrency.yaml<\/p>\n<pre class=\"wp-block-code\"><code>name: 19 - 1 - Managing Concurrency\n\non:\n  workflow_dispatch:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  ping:\n    runs-on: ubuntu-latest\n    steps:\n      - name: ping\n        run: |\n          echo \"Running workflow\"\n          sleep 15<\/code><\/pre>\n<h3 class=\"wp-block-heading\">Understanding Concurrency Groups<\/h3>\n<pre class=\"wp-block-code\"><code>concurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true<\/code><\/pre>\n<p class=\"wp-block-paragraph\">What this does:<\/p>\n<ul class=\"wp-block-list\"><li>Creates a concurrency group named after the workflow and branch<\/li><li>If another run with the same group starts, GitHub cancels the first one<\/li><li>This prevents multiple deployments to the same environment<\/li><\/ul>\n<p class=\"wp-block-paragraph\">The group name:<\/p>\n<ul class=\"wp-block-list\"><li>${{ github.workflow }} &#8211; Workflow name (&#8220;19 &#8211; 1 &#8211; Managing Concurrency&#8221;)<\/li><li>${{ github.ref }} &#8211; Branch name (&#8220;refs\/heads\/main&#8221;)<\/li><li>Combined: &#8220;19 &#8211; 1 &#8211; Managing Concurrency-refs\/heads\/main&#8221;<\/li><\/ul>\n<p class=\"wp-block-paragraph\">Example scenario:<\/p>\n<ol class=\"wp-block-list\"><li>I trigger the workflow on main branch &#8211; Run #1 starts<\/li><li>I trigger it again on main branch &#8211; Run #2 starts, Run #1 gets cancelled<\/li><li>I trigger it on dev branch &#8211; Run #3 starts and runs in parallel with Run #2 (different group!)<\/li><\/ol>\n<h3 class=\"wp-block-heading\">Job-Level Concurrency<\/h3>\n<p class=\"wp-block-paragraph\">You can also set concurrency at the job level:<\/p>\n<p class=\"wp-block-paragraph\">File: workflows\/19-2-concurrency.yaml<\/p>\n<pre class=\"wp-block-code\"><code>name: 19 - 2 - Managing Concurrency\n\non:\n  workflow_dispatch:\n\njobs:\n  ping-with-concurrency:\n    runs-on: ubuntu-latest\n    concurrency:\n      group: ${{ github.workflow }}-with-concurrency\n    steps:\n      - name: ping\n        run: |\n          echo \"Running job with concurrency\"\n          sleep 10\n  \n  ping-without-concurrency:\n    runs-on: ubuntu-latest\n    steps:\n      - name: ping\n        run: |\n          echo \"Running job without concurrency\"\n          sleep 10<\/code><\/pre>\n<h3 class=\"wp-block-heading\">The Difference<\/h3>\n<p class=\"wp-block-paragraph\"><strong>Workflow-level concurrency:<\/strong><\/p>\n<ul class=\"wp-block-list\"><li>Applies to the entire workflow<\/li><li>If a new run starts, ALL jobs in the old run get cancelled<\/li><\/ul>\n<p class=\"wp-block-paragraph\"><strong>Job-level concurrency:<\/strong><\/p>\n<ul class=\"wp-block-list\"><li>Only applies to that specific job<\/li><li>Other jobs in the workflow continue running<\/li><li>Useful when only certain jobs need concurrency control<\/li><\/ul>\n<p class=\"wp-block-paragraph\">In my example:<\/p>\n<ul class=\"wp-block-list\"><li>ping-with-concurrency &#8211; Only one instance runs at a time<\/li><li>ping-without-concurrency &#8211; Multiple instances can run in parallel<\/li><\/ul>\n<h3 class=\"wp-block-heading\">Advanced Concurrency: cancel-in-progress<\/h3>\n<p class=\"wp-block-paragraph\">By default, GitHub cancels the old run when a new one starts. But you can change this behavior:<\/p>\n<pre class=\"wp-block-code\"><code>concurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: false<\/code><\/pre>\n<p class=\"wp-block-paragraph\"><strong>cancel-in-progress: true (default):<\/strong><\/p>\n<ul class=\"wp-block-list\"><li>New run starts \u2192 Old run cancelled<\/li><li>Use this for deployments (you want the latest code deployed)<\/li><\/ul>\n<p class=\"wp-block-paragraph\"><strong>cancel-in-progress: false:<\/strong><\/p>\n<ul class=\"wp-block-list\"><li>New run starts \u2192 New run waits for old run to finish<\/li><li>Use this for expensive operations you don&#8217;t want to restart<\/li><\/ul>\n<h3 class=\"wp-block-heading\">Real-World Concurrency Patterns<\/h3>\n<h3 class=\"wp-block-heading\">Pattern 1: Deployment Concurrency by Environment<\/h3>\n<pre class=\"wp-block-code\"><code>name: Deploy Application\n\non:\n  push:\n    branches: [main]\n\njobs:\n  deploy-staging:\n    runs-on: ubuntu-latest\n    concurrency:\n      group: deploy-staging\n      cancel-in-progress: false\n    steps:\n      - run: echo \"Deploying to staging\"\n  \n  deploy-production:\n    runs-on: ubuntu-latest\n    concurrency:\n      group: deploy-production\n      cancel-in-progress: false\n    steps:\n      - run: echo \"Deploying to production\"<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This ensures:<\/p>\n<ul class=\"wp-block-list\"><li>Only one deployment to staging at a time<\/li><li>Only one deployment to production at a time<\/li><li>But staging and production deployments can run in parallel (different groups)<\/li><li>New deployments wait rather than cancel (cancel-in-progress: false)<\/li><\/ul>\n<h3 class=\"wp-block-heading\">Pattern 2: Per-Branch Concurrency<\/h3>\n<pre class=\"wp-block-code\"><code>name: Build and Test\n\non:\n  push:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    concurrency:\n      group: ${{ github.ref }}\n      cancel-in-progress: true\n    steps:\n      - run: echo \"Building ${{ github.ref }}\"<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This ensures:<\/p>\n<ul class=\"wp-block-list\"><li>Each branch has its own concurrency group<\/li><li>Multiple branches can build in parallel<\/li><li>New pushes to the same branch cancel old runs (you only care about the latest code)<\/li><\/ul>\n<h3 class=\"wp-block-heading\">Pattern 3: Global Lock for Shared Resources<\/h3>\n<pre class=\"wp-block-code\"><code>name: Database Migration\n\non:\n  push:\n\njobs:\n  migrate:\n    runs-on: ubuntu-latest\n    concurrency:\n      group: database-migration\n      cancel-in-progress: false\n    steps:\n      - run: echo \"Running migration\"<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This ensures:<\/p>\n<ul class=\"wp-block-list\"><li>Only ONE migration runs across ALL branches and workflows<\/li><li>Fixed group name means all runs share the same lock<\/li><li>Migrations queue up rather than cancel (critical for database changes)<\/li><\/ul>\n<h3 class=\"wp-block-heading\">Combining Reusable Workflows and Concurrency<\/h3>\n<p class=\"wp-block-paragraph\">Here&#8217;s how I combine both concepts for a production deployment:<\/p>\n<pre class=\"wp-block-code\"><code>name: Production Deployment Pipeline\n\non:\n  workflow_dispatch:\n\nconcurrency:\n  group: production-deployment\n  cancel-in-progress: false\n\njobs:\n  build:\n    uses: .\/.github\/workflows\/build.yaml\n  \n  test:\n    needs: build\n    uses: .\/.github\/workflows\/test.yaml\n  \n  deploy:\n    needs: test\n    uses: .\/.github\/workflows\/deploy.yaml\n    secrets:\n      access-token: ${{ secrets.GH_TOKEN }}<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This workflow:<\/p>\n<ol class=\"wp-block-list\"><li>Uses concurrency to prevent multiple production deployments<\/li><li>Calls reusable workflows for each deployment stage<\/li><li>Passes secrets to external workflows<\/li><li>Uses outputs from one reusable workflow as inputs to another<\/li><li>Won&#8217;t cancel deployments (cancel-in-progress: false)<\/li><\/ol>\n<h3 class=\"wp-block-heading\">What I Learned<\/h3>\n<h3 class=\"wp-block-heading\">About Reusable Workflows<\/h3>\n<ol class=\"wp-block-list\"><li><strong>Reusable workflows save enormous time:<\/strong> Instead of copying YAML across repos, I write it once and call it everywhere.<\/li><li><strong>Version your reusable workflows:<\/strong> Using @main is convenient but risky. For production, pin to a specific tag or SHA. This prevents breaking changes:<\/li><li><strong>Document your inputs and secrets:<\/strong> When others use your workflows, they need to know what inputs are required and what secrets to pass. I learned this the hard way when a teammate couldn&#8217;t figure out what the workflow expects.<\/li><li><strong>Test reusable workflows independently:<\/strong> Add workflow_dispatch to your reusable workflows so you can test them standalone before integrating them. List every secret you need.<\/li><li><strong>Keep reusable workflows focused:<\/strong> One workflow should do one thing well. A workflow that builds, tests, deploys, and notifies is too complex to reuse. Break it into smaller workflows that can be composed together. A workflow that tries to do everything becomes hard to reuse because different callers need different subsets. Better to have small, focused workflows: one for building, one for testing, one for deploying. Then compose them as needed. Test the reusable workflow standalone:<\/li><li><strong>Don&#8217;t nest too deep:<\/strong> Maximum 4 levels is more than enough. If you&#8217;re hitting that limit, your architecture is probably too complex. Simplify and flatten your workflow dependencies between workflows.<\/li><\/ol>\n<h3 class=\"wp-block-heading\">About Concurrency<\/h3>\n<ol class=\"wp-block-list\"><li><strong>Always use concurrency for deployments:<\/strong> Never let two deployments to the same environment run in parallel. This prevents race conditions and corrupted deployments.<\/li><li><strong>Choose cancel-in-progress wisely:<\/strong> Use true for fast operations where you only care about the latest code (builds, tests). Use false for critical operations that must complete (database migrations, deployments).<\/li><li><strong>Use descriptive group names:<\/strong> Include the workflow name and branch in your group names. This makes it clear what&#8217;s being locked and helps debug concurrency issues.<\/li><li><strong>Concurrency is not a queue:<\/strong> When cancel-in-progress is false, new runs wait. But if multiple runs are waiting, only the latest one will run after the current one finishes. The others get skipped.<\/li><li><strong>Test your concurrency behavior:<\/strong> Trigger multiple runs quickly and verify the behavior matches your expectations. I once had a deployment group that was too broad and it blocked unrelated workflows.<\/li><\/ol>\n<h3 class=\"wp-block-heading\">When to Use What<\/h3>\n<p class=\"wp-block-paragraph\">Use Custom Actions when:<\/p>\n<ul class=\"wp-block-list\"><li>Reusing steps within a job<\/li><li>The logic is small and focused<\/li><li>You need Docker or JavaScript capabilities<\/li><li>The reuse is within a single repository<\/li><\/ul>\n<p class=\"wp-block-paragraph\">Use Reusable Workflows when:<\/p>\n<ul class=\"wp-block-list\"><li>Reusing entire jobs or multi-job pipelines<\/li><li>Sharing workflows across repositories<\/li><li>The pipeline is complex with multiple stages<\/li><li>Example: Build \u2192 Test \u2192 Deploy \u2192 Verify<\/li><\/ul>\n<p class=\"wp-block-paragraph\">Use Concurrency when:<\/p>\n<ul class=\"wp-block-list\"><li>Preventing parallel runs to shared resources<\/li><li>Managing deployments to the same environment<\/li><li>Controlling resource usage (API limits, database locks)<\/li><li>Ensuring only the latest code gets deployed<\/li><\/ul>","protected":false},"excerpt":{"rendered":"<p>After building custom actions, I discovered an even more powerful concept: reusable workflows. While custom actions let you reuse steps, reusable workflows&hellip;<\/p>\n","protected":false},"author":2,"featured_media":78,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_rezaops_part":"6","_rezaops_linkedin_url":"https:\/\/www.linkedin.com\/pulse\/reusable-workflows-concurrency-github-actions-reza-chegini-h7xhc\/","footnotes":""},"categories":[27,10],"tags":[30,8,29,37],"series":[28],"class_list":["post-76","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ci-cd","category-devops","tag-ci-cd","tag-devops","tag-github-actions","tag-reusable-workflows","series-github-actions"],"jetpack_featured_media_url":"https:\/\/rezachegini.com\/wp-content\/uploads\/2026\/10\/reusable-workflows-concurrency-github-actions-cover.png","_links":{"self":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/76","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=76"}],"version-history":[{"count":1,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/76\/revisions"}],"predecessor-version":[{"id":77,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/76\/revisions\/77"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/media\/78"}],"wp:attachment":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=76"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=76"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=76"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fseries&post=76"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}